kasy-cli 1.34.0 → 1.36.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.
- package/README.md +1 -1
- package/bin/kasy.js +24 -2
- package/docs/cli-reference.md +7 -7
- package/lib/commands/new.js +11 -9
- package/lib/commands/release-version.js +234 -0
- package/lib/commands/update.js +27 -0
- package/lib/scaffold/CHANGELOG.json +18 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api.dart +24 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/api/authentication_api_interface.dart +15 -0
- package/lib/scaffold/backends/api/patch/lib/features/authentication/repositories/authentication_repository.dart +27 -0
- package/lib/scaffold/backends/api/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
- package/lib/scaffold/backends/firebase/setup-from-scratch.js +35 -21
- package/lib/scaffold/backends/patch-base-hashes.json +66 -0
- package/lib/scaffold/backends/supabase/edge-functions/stripe-webhook/index.ts +2 -0
- package/lib/scaffold/backends/supabase/migrations/20240101000007_welcome_notification.sql +36 -23
- package/lib/scaffold/backends/supabase/migrations/20240101000014_subscription_trial_end.sql +6 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/api/authentication_api.dart +82 -3
- package/lib/scaffold/backends/supabase/patch/lib/features/authentication/repositories/authentication_repository.dart +36 -0
- package/lib/scaffold/backends/supabase/patch/lib/features/subscriptions/api/entities/subscription_entity.dart +1 -0
- package/lib/scaffold/generate.js +53 -4
- package/lib/utils/i18n/messages-en.js +23 -0
- package/lib/utils/i18n/messages-es.js +23 -0
- package/lib/utils/i18n/messages-pt.js +23 -0
- package/package.json +5 -2
- package/templates/firebase/.firebase/hosting.YnVpbGQvd2Vi.cache +73 -0
- package/templates/firebase/AGENTS.md +83 -0
- package/templates/firebase/DESIGN_SYSTEM.md +37 -2
- package/templates/firebase/docs/auth-setup.en.md +2 -0
- package/templates/firebase/docs/auth-setup.es.md +2 -0
- package/templates/firebase/docs/auth-setup.pt.md +2 -0
- package/templates/firebase/firebase.json +56 -1
- package/templates/firebase/functions/src/authentication/triggers.ts +10 -6
- package/templates/firebase/functions/src/core/data/entities/subscription_entity.ts +5 -0
- package/templates/firebase/functions/src/core/data/entities/user_entity.ts +9 -1
- package/templates/firebase/functions/src/core/data/repositories/user_repository.ts +27 -0
- package/templates/firebase/functions/src/subscriptions/models/subscriptions.ts +3 -0
- package/templates/firebase/functions/src/subscriptions/stripe_functions.ts +4 -0
- package/templates/firebase/lib/components/kasy_alert.dart +0 -1
- package/templates/firebase/lib/components/kasy_app_bar.dart +31 -16
- package/templates/firebase/lib/components/kasy_bottom_sheet.dart +0 -1
- package/templates/firebase/lib/components/kasy_date_picker.dart +0 -5
- package/templates/firebase/lib/components/kasy_dialog.dart +0 -1
- package/templates/firebase/lib/components/kasy_otp_verification_bottom_sheet.dart +1 -1
- package/templates/firebase/lib/components/kasy_sidebar.dart +215 -178
- package/templates/firebase/lib/components/kasy_text_area.dart +0 -1
- package/templates/firebase/lib/components/kasy_text_field.dart +0 -1
- package/templates/firebase/lib/components/kasy_text_field_otp.dart +0 -1
- package/templates/firebase/lib/components/kasy_toast.dart +107 -41
- package/templates/firebase/lib/core/app_update/app_update_repository.dart +54 -0
- package/templates/firebase/lib/core/app_update/app_update_status.dart +14 -0
- package/templates/firebase/lib/core/app_update/update_available_sheet.dart +70 -0
- package/templates/firebase/lib/core/bottom_menu/active_tab_notifier.dart +40 -3
- package/templates/firebase/lib/core/bottom_menu/bottom_menu.dart +82 -34
- package/templates/firebase/lib/core/bottom_menu/web_content_wrapper.dart +6 -6
- package/templates/firebase/lib/core/bottom_menu/web_url.dart +20 -0
- package/templates/firebase/lib/core/data/api/remote_config_api.dart +38 -1
- package/templates/firebase/lib/core/guards/guard.dart +16 -2
- package/templates/firebase/lib/core/icons/kasy_icons.dart +3 -0
- package/templates/firebase/lib/core/rating/widgets/rate_banner.dart +2 -5
- package/templates/firebase/lib/core/rating/widgets/review_popup.dart +5 -3
- package/templates/firebase/lib/core/shared_preferences/shared_preferences.dart +14 -0
- package/templates/firebase/lib/core/states/components/maybe_show_update_available.dart +32 -0
- package/templates/firebase/lib/core/states/logout_action.dart +5 -1
- package/templates/firebase/lib/core/states/user_state_notifier.dart +85 -14
- package/templates/firebase/lib/core/theme/responsive_text_theme.dart +69 -0
- package/templates/firebase/lib/core/theme/texts.dart +90 -57
- package/templates/firebase/lib/core/theme/type_scale.dart +77 -0
- package/templates/firebase/lib/core/theme/web_background_sync_web.dart +20 -6
- package/templates/firebase/lib/core/utils/image_bytes_loader.dart +12 -0
- package/templates/firebase/lib/core/utils/image_bytes_loader_io.dart +28 -0
- package/templates/firebase/lib/core/utils/image_bytes_loader_web.dart +56 -0
- package/templates/firebase/lib/core/web_screen_width.dart +15 -0
- package/templates/firebase/lib/core/web_screen_width_io.dart +3 -0
- package/templates/firebase/lib/core/web_screen_width_web.dart +10 -0
- package/templates/firebase/lib/core/web_viewport_scale.dart +61 -25
- package/templates/firebase/lib/core/widgets/focus_visibility.dart +89 -0
- package/templates/firebase/lib/features/ai_chat/ai_chat_page.dart +1 -2
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_chat_conversation_view.dart +1 -2
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_list.dart +2 -3
- package/templates/firebase/lib/features/ai_chat/ui/widgets/ai_conversation_tile.dart +1 -2
- package/templates/firebase/lib/features/authentication/api/auth_web_support.dart +12 -0
- package/templates/firebase/lib/features/authentication/api/auth_web_support_web.dart +25 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api.dart +205 -0
- package/templates/firebase/lib/features/authentication/api/authentication_api_interface.dart +22 -0
- package/templates/firebase/lib/features/authentication/navigation/post_login_navigation.dart +32 -0
- package/templates/firebase/lib/features/authentication/providers/phone_auth_notifier.dart +7 -7
- package/templates/firebase/lib/features/authentication/providers/signin_state_provider.dart +34 -10
- package/templates/firebase/lib/features/authentication/providers/signup_state_provider.dart +2 -2
- package/templates/firebase/lib/features/authentication/repositories/authentication_repository.dart +37 -0
- package/templates/firebase/lib/features/authentication/repositories/exceptions/authentication_exceptions.dart +8 -0
- package/templates/firebase/lib/features/authentication/ui/components/otp_verification.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/components/phone_input.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/signin_page.dart +59 -0
- package/templates/firebase/lib/features/authentication/ui/widgets/auth_card_scaffold.dart +2 -2
- package/templates/firebase/lib/features/authentication/ui/widgets/recover_password_result.dart +2 -2
- package/templates/firebase/lib/features/feedbacks/ui/widgets/add_feature_button.dart +0 -1
- package/templates/firebase/lib/features/home/design_system_page.dart +134 -67
- package/templates/firebase/lib/features/home/home_components_page.dart +4 -3
- package/templates/firebase/lib/features/home/home_components_preview_page.dart +1 -3
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +226 -105
- package/templates/firebase/lib/features/home/home_page.dart +4 -0
- package/templates/firebase/lib/features/local_reminders/ui/reminder_page.dart +8 -3
- package/templates/firebase/lib/features/notifications/providers/notifications_provider.dart +10 -0
- package/templates/firebase/lib/features/notifications/ui/notifications_page.dart +3 -1
- package/templates/firebase/lib/features/notifications/ui/widgets/notification_tile.dart +8 -3
- package/templates/firebase/lib/features/onboarding/providers/onboarding_model.dart +11 -0
- package/templates/firebase/lib/features/onboarding/providers/onboarding_provider.dart +30 -3
- package/templates/firebase/lib/features/onboarding/ui/components/onboarding_loader.dart +13 -4
- package/templates/firebase/lib/features/settings/settings_page.dart +152 -11
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_page.dart +43 -15
- package/templates/firebase/lib/features/settings/ui/components/admin/admin_users_tab.dart +12 -12
- package/templates/firebase/lib/features/settings/ui/components/admin/send_push_notification_page.dart +1 -4
- package/templates/firebase/lib/features/settings/ui/components/create_password_sheet.dart +141 -0
- package/templates/firebase/lib/features/settings/ui/components/language_switcher.dart +0 -1
- package/templates/firebase/lib/features/settings/ui/widgets/admin_card.dart +42 -38
- package/templates/firebase/lib/features/settings/ui/widgets/settings_tile.dart +17 -2
- package/templates/firebase/lib/features/subscriptions/api/entities/subscription_entity.dart +3 -0
- package/templates/firebase/lib/features/subscriptions/api/stripe_product.dart +12 -11
- package/templates/firebase/lib/features/subscriptions/repositories/subscription_repository.dart +25 -2
- package/templates/firebase/lib/features/subscriptions/ui/component/active_premium_content.dart +319 -143
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_minimal.dart +1 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_row.dart +1 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/paywall_with_switch.dart +2 -5
- package/templates/firebase/lib/features/subscriptions/ui/component/premium_content.dart +1 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/feature_line.dart +1 -2
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_banner.dart +2 -7
- package/templates/firebase/lib/features/subscriptions/ui/widgets/premium_feature.dart +2 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_col.dart +1 -4
- package/templates/firebase/lib/features/subscriptions/ui/widgets/selectable_row.dart +0 -3
- package/templates/firebase/lib/i18n/en.i18n.json +49 -3
- package/templates/firebase/lib/i18n/es.i18n.json +49 -3
- package/templates/firebase/lib/i18n/pt.i18n.json +49 -3
- package/templates/firebase/lib/main.dart +11 -2
- package/templates/firebase/lib/router.dart +92 -13
- package/templates/firebase/pubspec.yaml +1 -1
- package/templates/firebase/test/core/data/api/fake_remote_config_api.dart +14 -0
- package/templates/firebase/test/core/states/user_state_notifier_test.dart +47 -3
- package/templates/firebase/test/core/web_viewport_scale_test.dart +68 -0
- package/templates/firebase/test/features/authentication/data/api/auth_api_fake.dart +15 -0
- package/templates/firebase/web/index.html +162 -14
- package/templates/firebase/lib/core/guards/user_info_guard.dart +0 -61
|
@@ -3,8 +3,10 @@ import 'dart:async';
|
|
|
3
3
|
import 'package:bart/bart/bart_model.dart';
|
|
4
4
|
import 'package:bart/bart/widgets/side_bar/custom_sidebar.dart';
|
|
5
5
|
import 'package:flutter/material.dart';
|
|
6
|
+
import 'package:kasy_kit/components/kasy_app_bar.dart';
|
|
6
7
|
import 'package:kasy_kit/components/kasy_avatar.dart';
|
|
7
8
|
import 'package:kasy_kit/components/kasy_avatar_presets.dart';
|
|
9
|
+
import 'package:kasy_kit/components/kasy_tabs.dart';
|
|
8
10
|
import 'package:kasy_kit/core/theme/theme.dart';
|
|
9
11
|
import 'package:kasy_kit/core/widgets/kasy_hover.dart';
|
|
10
12
|
import 'package:kasy_kit/i18n/translations.g.dart';
|
|
@@ -15,8 +17,11 @@ import 'package:kasy_kit/i18n/translations.g.dart';
|
|
|
15
17
|
|
|
16
18
|
// Figma sidebar is 223 wide; we run a touch wider for breathing room.
|
|
17
19
|
const double _kWidthOpen = 248.0;
|
|
18
|
-
const double _kWidthCollapsed =
|
|
20
|
+
const double _kWidthCollapsed = 64.0;
|
|
19
21
|
const double _kPadH = 16.0; // px-4
|
|
22
|
+
// Tighter horizontal gutter for the narrow collapsed rail, so a 64px rail keeps
|
|
23
|
+
// the 20px icons (44px active pill) centered without clipping.
|
|
24
|
+
const double _kCollapsedPadH = 10.0;
|
|
20
25
|
const double _kPadBottom = 16.0; // pb-4
|
|
21
26
|
// Top band that holds the logo. Equals the web header height (kasyWebHeaderHeight
|
|
22
27
|
// = 68) so the sidebar's first divider lines up with the header's bottom border.
|
|
@@ -56,7 +61,6 @@ class _SidebarColors {
|
|
|
56
61
|
required this.border,
|
|
57
62
|
required this.divider,
|
|
58
63
|
required this.activeBg,
|
|
59
|
-
required this.segmentThumb,
|
|
60
64
|
required this.textMuted,
|
|
61
65
|
required this.textActive,
|
|
62
66
|
required this.logout,
|
|
@@ -65,9 +69,9 @@ class _SidebarColors {
|
|
|
65
69
|
|
|
66
70
|
/// Maps the HeroUI Figma tokens onto the global [KasyColors]:
|
|
67
71
|
/// `surface → surface`, `foreground → onSurface`, `foreground/muted → muted`,
|
|
68
|
-
/// `border → border`, `separator → separator`, `default →
|
|
69
|
-
/// (hover/active fill
|
|
70
|
-
///
|
|
72
|
+
/// `border → border`, `separator → separator`, and `default →
|
|
73
|
+
/// surfaceNeutralSoft` (hover/active fill). The segmented control is the
|
|
74
|
+
/// shared [KasyTabs] component, which owns its own selected-thumb token.
|
|
71
75
|
factory _SidebarColors.fromContext(BuildContext context) {
|
|
72
76
|
final c = context.colors;
|
|
73
77
|
final bool dark = context.isDark;
|
|
@@ -79,8 +83,6 @@ class _SidebarColors {
|
|
|
79
83
|
divider: c.border,
|
|
80
84
|
// Hover / active item fill + tabs track + kbd chip (default/default).
|
|
81
85
|
activeBg: c.surfaceNeutralSoft,
|
|
82
|
-
// Selected segment thumb (HeroUI `segment`): lifts off the track.
|
|
83
|
-
segmentThumb: c.segment,
|
|
84
86
|
textMuted: c.muted,
|
|
85
87
|
textActive: c.onSurface,
|
|
86
88
|
logout: c.error,
|
|
@@ -92,7 +94,6 @@ class _SidebarColors {
|
|
|
92
94
|
final Color border;
|
|
93
95
|
final Color divider;
|
|
94
96
|
final Color activeBg;
|
|
95
|
-
final Color segmentThumb;
|
|
96
97
|
final Color textMuted;
|
|
97
98
|
final Color textActive;
|
|
98
99
|
final Color logout;
|
|
@@ -203,6 +204,8 @@ class KasySidebar extends StatefulWidget {
|
|
|
203
204
|
this.onSettingsTap,
|
|
204
205
|
this.onLogout,
|
|
205
206
|
this.initiallyCollapsed = false,
|
|
207
|
+
this.isDrawer = false,
|
|
208
|
+
this.showSearch = false,
|
|
206
209
|
this.side = KasySidebarSide.left,
|
|
207
210
|
this.routes,
|
|
208
211
|
this.onTapItem,
|
|
@@ -250,6 +253,16 @@ class KasySidebar extends StatefulWidget {
|
|
|
250
253
|
/// Whether the sidebar starts in the narrow (icon-only) mode.
|
|
251
254
|
final bool initiallyCollapsed;
|
|
252
255
|
|
|
256
|
+
/// Present the rail as a slide-in drawer (the mobile pattern): it always opens
|
|
257
|
+
/// wide and hides the collapse toggle, regardless of viewport width. Use when
|
|
258
|
+
/// opening the sidebar from an app-bar menu button on a phone.
|
|
259
|
+
final bool isDrawer;
|
|
260
|
+
|
|
261
|
+
/// Whether to pin a ⌘K search row above the footer in connected mode. Opt-in
|
|
262
|
+
/// (off by default) so the live app navigation stays lean; flip it on when the
|
|
263
|
+
/// sidebar should double as a command/search entry point.
|
|
264
|
+
final bool showSearch;
|
|
265
|
+
|
|
253
266
|
/// The screen edge this sidebar is anchored to.
|
|
254
267
|
final KasySidebarSide side;
|
|
255
268
|
|
|
@@ -269,11 +282,14 @@ class KasySidebar extends StatefulWidget {
|
|
|
269
282
|
}
|
|
270
283
|
|
|
271
284
|
class _KasySidebarState extends State<KasySidebar> {
|
|
272
|
-
// User's explicit open/close preference
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
//
|
|
276
|
-
|
|
285
|
+
// User's explicit open/close preference, set by tapping the toggle button.
|
|
286
|
+
// Null until they touch it, so the rail follows the viewport (auto-collapse on
|
|
287
|
+
// narrow). Once set, the explicit choice wins over the viewport — which is
|
|
288
|
+
// what lets a narrow-viewport rail be reopened by tapping the toggle.
|
|
289
|
+
bool? _collapsePreference;
|
|
290
|
+
|
|
291
|
+
// Computed at the start of build() — the user's explicit preference when set,
|
|
292
|
+
// otherwise the viewport auto-collapse. All submethods read this field.
|
|
277
293
|
bool _collapsed = false;
|
|
278
294
|
|
|
279
295
|
bool _incomeExpanded = false;
|
|
@@ -286,6 +302,19 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
286
302
|
/// Viewport width below which the sidebar auto-collapses (tablet breakpoint).
|
|
287
303
|
static const double _kBreakpoint = 1024.0;
|
|
288
304
|
|
|
305
|
+
/// Below this (mobile), the rail is meant to be a wide drawer: it always opens
|
|
306
|
+
/// full width and hides the collapse toggle (thinning makes no sense on a
|
|
307
|
+
/// phone-width sheet). The collapse affordance only exists from tablet up.
|
|
308
|
+
static const double _kMobileBreakpoint = 768.0;
|
|
309
|
+
|
|
310
|
+
bool _isMobile(BuildContext context) =>
|
|
311
|
+
MediaQuery.sizeOf(context).width < _kMobileBreakpoint;
|
|
312
|
+
|
|
313
|
+
/// Drawer presentation — always wide, no collapse toggle. True on a phone-
|
|
314
|
+
/// width viewport or when explicitly opened as a drawer ([isDrawer]).
|
|
315
|
+
bool _wideDrawer(BuildContext context) =>
|
|
316
|
+
widget.isDrawer || _isMobile(context);
|
|
317
|
+
|
|
289
318
|
/// True when wired to Bart's navigation (real, tappable screens).
|
|
290
319
|
bool get _connected =>
|
|
291
320
|
widget.routes != null &&
|
|
@@ -296,7 +325,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
296
325
|
@override
|
|
297
326
|
void initState() {
|
|
298
327
|
super.initState();
|
|
299
|
-
|
|
328
|
+
_collapsePreference = widget.initiallyCollapsed ? true : null;
|
|
300
329
|
// Connected mode follows Bart's currentItem (empty highlight here); the
|
|
301
330
|
// showcase defaults to the active layer from the Figma reference.
|
|
302
331
|
_activeItemId = _connected ? '' : 'object2';
|
|
@@ -305,13 +334,19 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
305
334
|
bool _isViewportNarrow(BuildContext context) =>
|
|
306
335
|
MediaQuery.sizeOf(context).width < _kBreakpoint;
|
|
307
336
|
|
|
337
|
+
/// Horizontal gutter for the rail content — tighter when collapsed so the
|
|
338
|
+
/// icon rail stays narrow while keeping the icons centered.
|
|
339
|
+
double get _railPadH => _collapsed ? _kCollapsedPadH : _kPadH;
|
|
340
|
+
|
|
308
341
|
_SidebarColors get _colors => _SidebarColors.fromContext(context);
|
|
309
342
|
|
|
310
343
|
// ── Actions ───────────────────────────────────────────────────────────────
|
|
311
344
|
|
|
312
|
-
//
|
|
345
|
+
// Flip the currently visible state and pin it as the explicit preference, so
|
|
346
|
+
// it overrides the viewport auto-collapse — this is what lets a narrow-
|
|
347
|
+
// viewport rail be expanded back open from the toggle.
|
|
313
348
|
void _toggleCollapse() =>
|
|
314
|
-
setState(() =>
|
|
349
|
+
setState(() => _collapsePreference = !_collapsed);
|
|
315
350
|
|
|
316
351
|
/// Navigates to a real route via Bart and clears any static-item highlight.
|
|
317
352
|
/// Moving to another screen also collapses an open submenu (e.g. Income) and
|
|
@@ -347,7 +382,10 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
347
382
|
|
|
348
383
|
@override
|
|
349
384
|
Widget build(BuildContext context) {
|
|
350
|
-
|
|
385
|
+
// A drawer / phone-width rail always opens wide; otherwise honour the user's
|
|
386
|
+
// explicit choice, falling back to the tablet auto-collapse.
|
|
387
|
+
_collapsed = !_wideDrawer(context) &&
|
|
388
|
+
(_collapsePreference ?? _isViewportNarrow(context));
|
|
351
389
|
|
|
352
390
|
final c = _colors;
|
|
353
391
|
final bool anchoredLeft = widget.side == KasySidebarSide.left;
|
|
@@ -409,11 +447,15 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
409
447
|
// Nav: workspace selector + segmented tabs + the layers list.
|
|
410
448
|
Expanded(
|
|
411
449
|
child: Padding(
|
|
412
|
-
padding:
|
|
450
|
+
padding: EdgeInsets.symmetric(horizontal: _railPadH),
|
|
413
451
|
child: SingleChildScrollView(
|
|
414
452
|
child: Column(
|
|
415
453
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
416
454
|
children: [
|
|
455
|
+
// Top gap lives INSIDE the scroll view so the list scrolls
|
|
456
|
+
// flush under the top divider (symmetric with the bottom),
|
|
457
|
+
// instead of clipping a gap below it.
|
|
458
|
+
const SizedBox(height: _kDividerGap),
|
|
417
459
|
if (!_collapsed) ...[
|
|
418
460
|
_buildWorkspaceSelector(c),
|
|
419
461
|
const SizedBox(height: _kHeaderGap),
|
|
@@ -430,10 +472,10 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
430
472
|
// Pinned ⌘K search row + profile block.
|
|
431
473
|
_buildDivider(c),
|
|
432
474
|
Padding(
|
|
433
|
-
padding:
|
|
434
|
-
|
|
475
|
+
padding: EdgeInsets.fromLTRB(
|
|
476
|
+
_railPadH,
|
|
435
477
|
_kFooterGap,
|
|
436
|
-
|
|
478
|
+
_railPadH,
|
|
437
479
|
_kPadBottom,
|
|
438
480
|
),
|
|
439
481
|
child: Column(
|
|
@@ -481,11 +523,15 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
481
523
|
_buildDivider(c),
|
|
482
524
|
Expanded(
|
|
483
525
|
child: Padding(
|
|
484
|
-
padding:
|
|
526
|
+
padding: EdgeInsets.symmetric(horizontal: _railPadH),
|
|
485
527
|
child: SingleChildScrollView(
|
|
486
528
|
child: Column(
|
|
487
529
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
488
530
|
children: [
|
|
531
|
+
// Top gap lives INSIDE the scroll view so the list scrolls
|
|
532
|
+
// flush under the top divider (symmetric with the bottom),
|
|
533
|
+
// instead of clipping a gap below it.
|
|
534
|
+
const SizedBox(height: _kDividerGap),
|
|
489
535
|
if (!_collapsed) ...[
|
|
490
536
|
_buildSectionLabel('MAIN', c),
|
|
491
537
|
const SizedBox(height: _kItemGap),
|
|
@@ -530,10 +576,19 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
530
576
|
_buildDivider(c),
|
|
531
577
|
const SizedBox(height: _kFooterGap),
|
|
532
578
|
Padding(
|
|
533
|
-
padding:
|
|
579
|
+
padding: EdgeInsets.fromLTRB(_railPadH, 0, _railPadH, _kPadBottom),
|
|
534
580
|
child: Column(
|
|
535
581
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
536
582
|
children: [
|
|
583
|
+
if (widget.showSearch)
|
|
584
|
+
_buildItemRow(
|
|
585
|
+
c,
|
|
586
|
+
icon: KasyIcons.search,
|
|
587
|
+
label: 'Search',
|
|
588
|
+
isActive: false,
|
|
589
|
+
onTap: () {},
|
|
590
|
+
trailing: [_buildKbd(c)],
|
|
591
|
+
),
|
|
537
592
|
_buildNavItem(context, _kHelpItem, c),
|
|
538
593
|
_buildItemRow(
|
|
539
594
|
c,
|
|
@@ -559,25 +614,40 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
559
614
|
/// header height) so the divider underneath lines up with the header's bottom
|
|
560
615
|
/// border. Content is vertically centred, mirroring the header's toolbar row.
|
|
561
616
|
Widget _buildTopBand(_SidebarColors c) {
|
|
617
|
+
// Keep the first divider on the same line as the content chrome's bottom
|
|
618
|
+
// border: the web header (68) on desktop, but the shorter KasyAppBar on
|
|
619
|
+
// tablet (medium), where the page keeps its own app bar instead of the
|
|
620
|
+
// header. Without this the line breaks between the rail and the app bar.
|
|
621
|
+
final double bandHeight =
|
|
622
|
+
MediaQuery.sizeOf(context).width >= _kBreakpoint
|
|
623
|
+
? _kTopBandHeight
|
|
624
|
+
: kasyAppBarBodyTopOverlap(context);
|
|
625
|
+
// No collapse toggle on the mobile / drawer rail (it always opens wide).
|
|
626
|
+
final bool showToggle = !_wideDrawer(context);
|
|
627
|
+
final bool anchoredLeft = widget.side == KasySidebarSide.left;
|
|
628
|
+
// Brand wordmark — same artwork as the splash screen.
|
|
629
|
+
final Widget logo = Image.asset(
|
|
630
|
+
c.isDark
|
|
631
|
+
? 'assets/images/logo_wordmark_dark.png'
|
|
632
|
+
: 'assets/images/logo_wordmark_light.png',
|
|
633
|
+
height: 32,
|
|
634
|
+
fit: BoxFit.contain,
|
|
635
|
+
);
|
|
636
|
+
// Left rail: wordmark then toggle (toggle hugs the content edge). The right
|
|
637
|
+
// rail mirrors it so the toggle still hugs the content edge (now the left).
|
|
638
|
+
final List<Widget> rowChildren = <Widget>[
|
|
639
|
+
logo,
|
|
640
|
+
if (showToggle) ...[const Spacer(), _buildToggleButton(c)],
|
|
641
|
+
];
|
|
562
642
|
return Padding(
|
|
563
|
-
padding:
|
|
643
|
+
padding: EdgeInsets.symmetric(horizontal: _railPadH),
|
|
564
644
|
child: SizedBox(
|
|
565
|
-
height:
|
|
645
|
+
height: bandHeight,
|
|
566
646
|
child: _collapsed
|
|
567
647
|
? Center(child: _buildToggleButton(c))
|
|
568
648
|
: Row(
|
|
569
|
-
children:
|
|
570
|
-
|
|
571
|
-
Image.asset(
|
|
572
|
-
c.isDark
|
|
573
|
-
? 'assets/images/logo_wordmark_dark.png'
|
|
574
|
-
: 'assets/images/logo_wordmark_light.png',
|
|
575
|
-
height: 32,
|
|
576
|
-
fit: BoxFit.contain,
|
|
577
|
-
),
|
|
578
|
-
const Spacer(),
|
|
579
|
-
_buildToggleButton(c),
|
|
580
|
-
],
|
|
649
|
+
children:
|
|
650
|
+
anchoredLeft ? rowChildren : rowChildren.reversed.toList(),
|
|
581
651
|
),
|
|
582
652
|
),
|
|
583
653
|
);
|
|
@@ -618,10 +688,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
618
688
|
'3D Dog Character',
|
|
619
689
|
maxLines: 1,
|
|
620
690
|
overflow: TextOverflow.ellipsis,
|
|
621
|
-
style:
|
|
622
|
-
fontSize: 14,
|
|
623
|
-
height: 20 / 14,
|
|
624
|
-
fontWeight: FontWeight.w500,
|
|
691
|
+
style: context.kasyTextTheme.rowTitle.copyWith(
|
|
625
692
|
color: c.textActive,
|
|
626
693
|
),
|
|
627
694
|
),
|
|
@@ -635,10 +702,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
635
702
|
'3D Design Project',
|
|
636
703
|
maxLines: 1,
|
|
637
704
|
overflow: TextOverflow.ellipsis,
|
|
638
|
-
style:
|
|
639
|
-
fontSize: 12,
|
|
640
|
-
height: 16 / 12,
|
|
641
|
-
fontWeight: FontWeight.w400,
|
|
705
|
+
style: context.kasyTextTheme.cardSubtitle.copyWith(
|
|
642
706
|
color: c.textMuted,
|
|
643
707
|
),
|
|
644
708
|
),
|
|
@@ -648,72 +712,22 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
648
712
|
|
|
649
713
|
// ── Segmented tabs (Layers / Assets) ────────────────────────────────────────
|
|
650
714
|
|
|
715
|
+
/// The showcase segment is the shared [KasyTabs] component (primary pill,
|
|
716
|
+
/// fill mode) so the sidebar demos the real design-system control rather than
|
|
717
|
+
/// a bespoke copy.
|
|
651
718
|
Widget _buildTabs(_SidebarColors c) {
|
|
652
|
-
return
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
borderRadius: BorderRadius.circular(24),
|
|
658
|
-
),
|
|
659
|
-
child: Row(
|
|
660
|
-
children: [
|
|
661
|
-
_buildTab(c, 'Layers', 0),
|
|
662
|
-
const SizedBox(width: 2),
|
|
663
|
-
_buildTab(c, 'Assets', 1),
|
|
664
|
-
],
|
|
665
|
-
),
|
|
666
|
-
);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
Widget _buildTab(_SidebarColors c, String label, int index) {
|
|
670
|
-
final bool selected = _showcaseTab == index;
|
|
671
|
-
return Expanded(
|
|
672
|
-
child: MouseRegion(
|
|
673
|
-
cursor: SystemMouseCursors.click,
|
|
674
|
-
child: GestureDetector(
|
|
675
|
-
behavior: HitTestBehavior.opaque,
|
|
676
|
-
onTap: () => setState(() => _showcaseTab = index),
|
|
677
|
-
child: AnimatedContainer(
|
|
678
|
-
duration: const Duration(milliseconds: 180),
|
|
679
|
-
curve: Curves.easeOut,
|
|
680
|
-
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
681
|
-
alignment: Alignment.center,
|
|
682
|
-
decoration: BoxDecoration(
|
|
683
|
-
color: selected ? c.segmentThumb : Colors.transparent,
|
|
684
|
-
borderRadius: BorderRadius.circular(24),
|
|
685
|
-
boxShadow: selected
|
|
686
|
-
? const [
|
|
687
|
-
BoxShadow(
|
|
688
|
-
color: Color(0x0F000000),
|
|
689
|
-
blurRadius: 8,
|
|
690
|
-
offset: Offset(0, 2),
|
|
691
|
-
),
|
|
692
|
-
]
|
|
693
|
-
: null,
|
|
694
|
-
),
|
|
695
|
-
child: Text(
|
|
696
|
-
label,
|
|
697
|
-
style: TextStyle(
|
|
698
|
-
fontSize: 14,
|
|
699
|
-
height: 20 / 14,
|
|
700
|
-
fontWeight: FontWeight.w500,
|
|
701
|
-
color: selected ? c.textActive : c.textMuted,
|
|
702
|
-
),
|
|
703
|
-
),
|
|
704
|
-
),
|
|
705
|
-
),
|
|
706
|
-
),
|
|
719
|
+
return KasyTabs(
|
|
720
|
+
tabs: const ['Layers', 'Assets'],
|
|
721
|
+
selectedIndex: _showcaseTab,
|
|
722
|
+
onTabSelected: (index) => setState(() => _showcaseTab = index),
|
|
723
|
+
mode: KasyTabsMode.fill,
|
|
707
724
|
);
|
|
708
725
|
}
|
|
709
726
|
|
|
710
727
|
// ── ⌘K keyboard chip ─────────────────────────────────────────────────────────
|
|
711
728
|
|
|
712
729
|
Widget _buildKbd(_SidebarColors c) {
|
|
713
|
-
final TextStyle style =
|
|
714
|
-
fontSize: 14,
|
|
715
|
-
height: 20 / 14,
|
|
716
|
-
fontWeight: FontWeight.w500,
|
|
730
|
+
final TextStyle style = context.kasyTextTheme.rowTitle.copyWith(
|
|
717
731
|
color: c.textMuted,
|
|
718
732
|
);
|
|
719
733
|
return Container(
|
|
@@ -774,10 +788,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
774
788
|
widget.profileName,
|
|
775
789
|
maxLines: 1,
|
|
776
790
|
overflow: TextOverflow.ellipsis,
|
|
777
|
-
style:
|
|
778
|
-
fontSize: 14,
|
|
779
|
-
height: 20 / 14,
|
|
780
|
-
fontWeight: FontWeight.w500,
|
|
791
|
+
style: context.kasyTextTheme.rowTitle.copyWith(
|
|
781
792
|
color: c.textActive,
|
|
782
793
|
),
|
|
783
794
|
),
|
|
@@ -785,10 +796,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
785
796
|
widget.profileEmail,
|
|
786
797
|
maxLines: 1,
|
|
787
798
|
overflow: TextOverflow.ellipsis,
|
|
788
|
-
style:
|
|
789
|
-
fontSize: 12,
|
|
790
|
-
height: 16 / 12,
|
|
791
|
-
fontWeight: FontWeight.w500,
|
|
799
|
+
style: context.textTheme.labelMedium?.copyWith(
|
|
792
800
|
color: c.textMuted,
|
|
793
801
|
),
|
|
794
802
|
),
|
|
@@ -816,11 +824,8 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
816
824
|
padding: const EdgeInsets.only(left: _kItemHPad),
|
|
817
825
|
child: Text(
|
|
818
826
|
label,
|
|
819
|
-
style:
|
|
820
|
-
fontSize: 11,
|
|
821
|
-
fontWeight: FontWeight.w600,
|
|
827
|
+
style: context.kasyTextTheme.sectionLabel.copyWith(
|
|
822
828
|
color: c.textMuted,
|
|
823
|
-
letterSpacing: 0.6,
|
|
824
829
|
),
|
|
825
830
|
),
|
|
826
831
|
);
|
|
@@ -913,6 +918,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
913
918
|
iconColor: iconColor,
|
|
914
919
|
activeBg: c.activeBg,
|
|
915
920
|
colors: c,
|
|
921
|
+
anchoredLeft: widget.side == KasySidebarSide.left,
|
|
916
922
|
onTap: onTap,
|
|
917
923
|
),
|
|
918
924
|
),
|
|
@@ -952,10 +958,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
952
958
|
label,
|
|
953
959
|
maxLines: 1,
|
|
954
960
|
overflow: TextOverflow.ellipsis,
|
|
955
|
-
style:
|
|
956
|
-
fontSize: 14,
|
|
957
|
-
height: 20 / 14,
|
|
958
|
-
fontWeight: FontWeight.w500,
|
|
961
|
+
style: context.kasyTextTheme.rowTitle.copyWith(
|
|
959
962
|
color: labelColor,
|
|
960
963
|
),
|
|
961
964
|
),
|
|
@@ -989,6 +992,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
989
992
|
subItems: item.subItems,
|
|
990
993
|
activeSubItem: _activeSubItem,
|
|
991
994
|
colors: c,
|
|
995
|
+
anchoredLeft: widget.side == KasySidebarSide.left,
|
|
992
996
|
onSubItemTap: _activateSubItem,
|
|
993
997
|
),
|
|
994
998
|
);
|
|
@@ -1021,10 +1025,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
1021
1025
|
Expanded(
|
|
1022
1026
|
child: Text(
|
|
1023
1027
|
item.label,
|
|
1024
|
-
style:
|
|
1025
|
-
fontSize: 14,
|
|
1026
|
-
height: 20 / 14,
|
|
1027
|
-
fontWeight: FontWeight.w500,
|
|
1028
|
+
style: context.kasyTextTheme.rowTitle.copyWith(
|
|
1028
1029
|
color: c.textActive,
|
|
1029
1030
|
),
|
|
1030
1031
|
),
|
|
@@ -1141,8 +1142,7 @@ class _KasySidebarState extends State<KasySidebar> {
|
|
|
1141
1142
|
alignment: Alignment.centerLeft,
|
|
1142
1143
|
child: Text(
|
|
1143
1144
|
label,
|
|
1144
|
-
style:
|
|
1145
|
-
fontSize: 12,
|
|
1145
|
+
style: context.textTheme.labelMedium?.copyWith(
|
|
1146
1146
|
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
|
|
1147
1147
|
color: textColor,
|
|
1148
1148
|
letterSpacing: -0.24,
|
|
@@ -1169,6 +1169,7 @@ class _ProHoverPopupIcon extends StatefulWidget {
|
|
|
1169
1169
|
required this.subItems,
|
|
1170
1170
|
required this.activeSubItem,
|
|
1171
1171
|
required this.colors,
|
|
1172
|
+
required this.anchoredLeft,
|
|
1172
1173
|
required this.onSubItemTap,
|
|
1173
1174
|
});
|
|
1174
1175
|
|
|
@@ -1178,6 +1179,12 @@ class _ProHoverPopupIcon extends StatefulWidget {
|
|
|
1178
1179
|
final List<String> subItems;
|
|
1179
1180
|
final String activeSubItem;
|
|
1180
1181
|
final _SidebarColors colors;
|
|
1182
|
+
|
|
1183
|
+
/// Whether the rail is on the left. The popup opens toward the content side
|
|
1184
|
+
/// (right of the icon on a left rail, left of the icon on a right rail) so it
|
|
1185
|
+
/// never spills off the screen edge.
|
|
1186
|
+
final bool anchoredLeft;
|
|
1187
|
+
|
|
1181
1188
|
final ValueChanged<String> onSubItemTap;
|
|
1182
1189
|
|
|
1183
1190
|
@override
|
|
@@ -1236,14 +1243,15 @@ class _ProHoverPopupIconState extends State<_ProHoverPopupIcon> {
|
|
|
1236
1243
|
}
|
|
1237
1244
|
|
|
1238
1245
|
Widget _buildPopup(BuildContext context) {
|
|
1246
|
+
final bool left = widget.anchoredLeft;
|
|
1239
1247
|
return CompositedTransformFollower(
|
|
1240
1248
|
link: _layerLink,
|
|
1241
1249
|
showWhenUnlinked: false,
|
|
1242
|
-
targetAnchor: Alignment.centerRight,
|
|
1243
|
-
followerAnchor: Alignment.centerLeft,
|
|
1244
|
-
offset:
|
|
1250
|
+
targetAnchor: left ? Alignment.centerRight : Alignment.centerLeft,
|
|
1251
|
+
followerAnchor: left ? Alignment.centerLeft : Alignment.centerRight,
|
|
1252
|
+
offset: Offset(left ? 12 : -12, 0),
|
|
1245
1253
|
child: Align(
|
|
1246
|
-
alignment: Alignment.centerLeft,
|
|
1254
|
+
alignment: left ? Alignment.centerLeft : Alignment.centerRight,
|
|
1247
1255
|
child: MouseRegion(
|
|
1248
1256
|
onEnter: (_) => setState(() => _onPopup = true),
|
|
1249
1257
|
onExit: (_) {
|
|
@@ -1292,8 +1300,7 @@ class _ProHoverPopupIconState extends State<_ProHoverPopupIcon> {
|
|
|
1292
1300
|
),
|
|
1293
1301
|
child: Text(
|
|
1294
1302
|
label,
|
|
1295
|
-
style:
|
|
1296
|
-
fontSize: 12,
|
|
1303
|
+
style: context.textTheme.labelMedium?.copyWith(
|
|
1297
1304
|
fontWeight: isActive ? FontWeight.w600 : FontWeight.w500,
|
|
1298
1305
|
color: textColor,
|
|
1299
1306
|
letterSpacing: -0.24,
|
|
@@ -1320,6 +1327,7 @@ class _ProTooltipIcon extends StatefulWidget {
|
|
|
1320
1327
|
required this.iconColor,
|
|
1321
1328
|
required this.activeBg,
|
|
1322
1329
|
required this.colors,
|
|
1330
|
+
required this.anchoredLeft,
|
|
1323
1331
|
required this.onTap,
|
|
1324
1332
|
});
|
|
1325
1333
|
|
|
@@ -1329,6 +1337,12 @@ class _ProTooltipIcon extends StatefulWidget {
|
|
|
1329
1337
|
final Color iconColor;
|
|
1330
1338
|
final Color activeBg;
|
|
1331
1339
|
final _SidebarColors colors;
|
|
1340
|
+
|
|
1341
|
+
/// Whether the rail is on the left. The tooltip opens toward the content side
|
|
1342
|
+
/// (and its arrow points back at the icon) so it never spills off the screen
|
|
1343
|
+
/// edge on a right-anchored rail.
|
|
1344
|
+
final bool anchoredLeft;
|
|
1345
|
+
|
|
1332
1346
|
final VoidCallback onTap;
|
|
1333
1347
|
|
|
1334
1348
|
@override
|
|
@@ -1374,15 +1388,20 @@ class _ProTooltipIconState extends State<_ProTooltipIcon> {
|
|
|
1374
1388
|
}
|
|
1375
1389
|
|
|
1376
1390
|
Widget _buildTooltip(BuildContext context) {
|
|
1391
|
+
final bool left = widget.anchoredLeft;
|
|
1377
1392
|
return CompositedTransformFollower(
|
|
1378
1393
|
link: _layerLink,
|
|
1379
1394
|
showWhenUnlinked: false,
|
|
1380
|
-
targetAnchor: Alignment.centerRight,
|
|
1381
|
-
followerAnchor: Alignment.centerLeft,
|
|
1382
|
-
offset:
|
|
1395
|
+
targetAnchor: left ? Alignment.centerRight : Alignment.centerLeft,
|
|
1396
|
+
followerAnchor: left ? Alignment.centerLeft : Alignment.centerRight,
|
|
1397
|
+
offset: Offset(left ? 4 : -4, 0),
|
|
1383
1398
|
child: Align(
|
|
1384
|
-
alignment: Alignment.centerLeft,
|
|
1385
|
-
child: _TooltipCard(
|
|
1399
|
+
alignment: left ? Alignment.centerLeft : Alignment.centerRight,
|
|
1400
|
+
child: _TooltipCard(
|
|
1401
|
+
label: widget.label,
|
|
1402
|
+
colors: widget.colors,
|
|
1403
|
+
pointsLeft: left,
|
|
1404
|
+
),
|
|
1386
1405
|
),
|
|
1387
1406
|
);
|
|
1388
1407
|
}
|
|
@@ -1393,11 +1412,19 @@ class _ProTooltipIconState extends State<_ProTooltipIcon> {
|
|
|
1393
1412
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1394
1413
|
|
|
1395
1414
|
class _TooltipCard extends StatelessWidget {
|
|
1396
|
-
const _TooltipCard({
|
|
1415
|
+
const _TooltipCard({
|
|
1416
|
+
required this.label,
|
|
1417
|
+
required this.colors,
|
|
1418
|
+
this.pointsLeft = true,
|
|
1419
|
+
});
|
|
1397
1420
|
|
|
1398
1421
|
final String label;
|
|
1399
1422
|
final _SidebarColors colors;
|
|
1400
1423
|
|
|
1424
|
+
/// Arrow points back at the icon: left when the rail is on the left (tooltip
|
|
1425
|
+
/// sits to the icon's right), right when the rail is on the right.
|
|
1426
|
+
final bool pointsLeft;
|
|
1427
|
+
|
|
1401
1428
|
static const double _arrowW = 13.0;
|
|
1402
1429
|
static const double _arrowH = 26.0;
|
|
1403
1430
|
static const double _arrowOverlap = 8.0;
|
|
@@ -1411,43 +1438,44 @@ class _TooltipCard extends StatelessWidget {
|
|
|
1411
1438
|
final Color bg = colors.isDark ? colors.divider : colors.textActive;
|
|
1412
1439
|
final Color textColor = colors.isDark ? colors.textActive : colors.bg;
|
|
1413
1440
|
|
|
1441
|
+
final Widget arrow = SizedBox(
|
|
1442
|
+
width: _arrowW,
|
|
1443
|
+
height: _arrowH,
|
|
1444
|
+
child: CustomPaint(
|
|
1445
|
+
painter: _TooltipArrowPainter(color: bg, pointsLeft: pointsLeft),
|
|
1446
|
+
),
|
|
1447
|
+
);
|
|
1448
|
+
// Pull the card over the arrow's base so they read as one shape; the
|
|
1449
|
+
// direction of the overlap flips with the arrow side.
|
|
1450
|
+
final Widget card = Transform.translate(
|
|
1451
|
+
offset: Offset(pointsLeft ? -_arrowOverlap : _arrowOverlap, 0),
|
|
1452
|
+
child: Container(
|
|
1453
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
1454
|
+
decoration: BoxDecoration(
|
|
1455
|
+
color: bg,
|
|
1456
|
+
borderRadius: BorderRadius.circular(4),
|
|
1457
|
+
boxShadow: const [
|
|
1458
|
+
BoxShadow(
|
|
1459
|
+
color: Color(0x40000000),
|
|
1460
|
+
blurRadius: 5,
|
|
1461
|
+
offset: Offset(0, 5),
|
|
1462
|
+
),
|
|
1463
|
+
],
|
|
1464
|
+
),
|
|
1465
|
+
child: Text(
|
|
1466
|
+
label,
|
|
1467
|
+
style: context.textTheme.bodyMedium?.copyWith(
|
|
1468
|
+
color: textColor,
|
|
1469
|
+
),
|
|
1470
|
+
),
|
|
1471
|
+
),
|
|
1472
|
+
);
|
|
1473
|
+
|
|
1414
1474
|
return Material(
|
|
1415
1475
|
color: Colors.transparent,
|
|
1416
1476
|
child: Row(
|
|
1417
1477
|
mainAxisSize: MainAxisSize.min,
|
|
1418
|
-
children: [
|
|
1419
|
-
SizedBox(
|
|
1420
|
-
width: _arrowW,
|
|
1421
|
-
height: _arrowH,
|
|
1422
|
-
child: CustomPaint(painter: _TooltipArrowPainter(color: bg)),
|
|
1423
|
-
),
|
|
1424
|
-
Transform.translate(
|
|
1425
|
-
offset: const Offset(-_arrowOverlap, 0),
|
|
1426
|
-
child: Container(
|
|
1427
|
-
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
1428
|
-
decoration: BoxDecoration(
|
|
1429
|
-
color: bg,
|
|
1430
|
-
borderRadius: BorderRadius.circular(4),
|
|
1431
|
-
boxShadow: const [
|
|
1432
|
-
BoxShadow(
|
|
1433
|
-
color: Color(0x40000000),
|
|
1434
|
-
blurRadius: 5,
|
|
1435
|
-
offset: Offset(0, 5),
|
|
1436
|
-
),
|
|
1437
|
-
],
|
|
1438
|
-
),
|
|
1439
|
-
child: Text(
|
|
1440
|
-
label,
|
|
1441
|
-
style: TextStyle(
|
|
1442
|
-
fontSize: 14,
|
|
1443
|
-
fontWeight: FontWeight.w400,
|
|
1444
|
-
color: textColor,
|
|
1445
|
-
height: 20 / 14,
|
|
1446
|
-
),
|
|
1447
|
-
),
|
|
1448
|
-
),
|
|
1449
|
-
),
|
|
1450
|
-
],
|
|
1478
|
+
children: pointsLeft ? <Widget>[arrow, card] : <Widget>[card, arrow],
|
|
1451
1479
|
),
|
|
1452
1480
|
);
|
|
1453
1481
|
}
|
|
@@ -1458,22 +1486,31 @@ class _TooltipCard extends StatelessWidget {
|
|
|
1458
1486
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
1459
1487
|
|
|
1460
1488
|
class _TooltipArrowPainter extends CustomPainter {
|
|
1461
|
-
const _TooltipArrowPainter({required this.color});
|
|
1489
|
+
const _TooltipArrowPainter({required this.color, this.pointsLeft = true});
|
|
1462
1490
|
final Color color;
|
|
1491
|
+
final bool pointsLeft;
|
|
1463
1492
|
|
|
1464
1493
|
@override
|
|
1465
1494
|
void paint(Canvas canvas, Size size) {
|
|
1466
1495
|
final paint = Paint()
|
|
1467
1496
|
..color = color
|
|
1468
1497
|
..style = PaintingStyle.fill;
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1498
|
+
// Apex on the side it points to; base on the opposite (card) side.
|
|
1499
|
+
final path = pointsLeft
|
|
1500
|
+
? (Path()
|
|
1501
|
+
..moveTo(size.width, 0)
|
|
1502
|
+
..lineTo(0, size.height / 2)
|
|
1503
|
+
..lineTo(size.width, size.height)
|
|
1504
|
+
..close())
|
|
1505
|
+
: (Path()
|
|
1506
|
+
..moveTo(0, 0)
|
|
1507
|
+
..lineTo(size.width, size.height / 2)
|
|
1508
|
+
..lineTo(0, size.height)
|
|
1509
|
+
..close());
|
|
1474
1510
|
canvas.drawPath(path, paint);
|
|
1475
1511
|
}
|
|
1476
1512
|
|
|
1477
1513
|
@override
|
|
1478
|
-
bool shouldRepaint(_TooltipArrowPainter old) =>
|
|
1514
|
+
bool shouldRepaint(_TooltipArrowPainter old) =>
|
|
1515
|
+
old.color != color || old.pointsLeft != pointsLeft;
|
|
1479
1516
|
}
|