kasy-cli 1.19.0 → 1.19.3
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/bin/kasy.js +3 -2
- package/lib/commands/run.js +30 -6
- package/lib/utils/flutter-run.js +16 -4
- package/lib/utils/i18n/messages-en.js +2 -0
- package/lib/utils/i18n/messages-es.js +2 -0
- package/lib/utils/i18n/messages-pt.js +2 -0
- package/package.json +1 -1
- package/templates/firebase/lib/components/kasy_text_field.dart +11 -4
- package/templates/firebase/lib/core/dev_inspector/dev_inspector.dart +20 -3
- package/templates/firebase/lib/features/home/home_components_preview_registry.dart +81 -13
package/bin/kasy.js
CHANGED
|
@@ -354,8 +354,9 @@ function buildProgram(language) {
|
|
|
354
354
|
.argument('[directory]', 'Project folder (default: current directory)', '.')
|
|
355
355
|
.option('--ios', 'Run on iOS simulator/device')
|
|
356
356
|
.option('--android', 'Run on Android emulator/device')
|
|
357
|
-
.option('--web', 'Run on
|
|
358
|
-
.option('--
|
|
357
|
+
.option('--web', 'Run on web — prints localhost URL in lime so you open it in your own browser (your extensions, your accounts)')
|
|
358
|
+
.option('--open', 'With --web: auto-launch a clean Chrome window (Flutter profile, no extensions) instead of just printing the URL')
|
|
359
|
+
.option('--web-port <port>', 'Fixed port for web (default 5555) — keeps the origin stable so Firebase Auth sessions persist between runs')
|
|
359
360
|
.option('-d, --device <id>', 'Run on specific device ID')
|
|
360
361
|
.option('--prod', 'Use production dart-defines (from launch.json)')
|
|
361
362
|
.option('--no-defines', 'Skip dart-defines from launch.json')
|
package/lib/commands/run.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs-extra');
|
|
|
4
4
|
const kleur = require('kleur');
|
|
5
5
|
const ui = require('../utils/ui');
|
|
6
6
|
const { createTranslator, detectDefaultLanguage } = require('../utils/i18n');
|
|
7
|
-
const { printCompactHeader } = require('../utils/brand');
|
|
7
|
+
const { printCompactHeader, paintLime } = require('../utils/brand');
|
|
8
8
|
const { spawnFlutterWithSpinner } = require('../utils/flutter-run');
|
|
9
9
|
|
|
10
10
|
function listFlutterDevices(projectDir) {
|
|
@@ -252,9 +252,19 @@ async function runRun(directory, options = {}) {
|
|
|
252
252
|
let resolvedDeviceLabel = null;
|
|
253
253
|
let pickedDevice = null;
|
|
254
254
|
let isChromeTarget = false;
|
|
255
|
+
let isWebServerTarget = false;
|
|
255
256
|
if (options.web) {
|
|
256
|
-
|
|
257
|
-
|
|
257
|
+
// Default: web-server (no auto-launched Chrome window with a throwaway
|
|
258
|
+
// profile). The URL is printed in lime so the user opens it in their own
|
|
259
|
+
// browser — keeping access to extensions, logged-in accounts, etc.
|
|
260
|
+
// Pass --open to fall back to flutter's auto-Chrome behavior.
|
|
261
|
+
if (options.open) {
|
|
262
|
+
isChromeTarget = true;
|
|
263
|
+
deviceArgs.push('-d', 'chrome');
|
|
264
|
+
} else {
|
|
265
|
+
isWebServerTarget = true;
|
|
266
|
+
deviceArgs.push('-d', 'web-server');
|
|
267
|
+
}
|
|
258
268
|
} else if (options.ios) {
|
|
259
269
|
deviceArgs.push('-d', 'ios');
|
|
260
270
|
} else if (options.android) {
|
|
@@ -281,7 +291,7 @@ async function runRun(directory, options = {}) {
|
|
|
281
291
|
}
|
|
282
292
|
}
|
|
283
293
|
|
|
284
|
-
if (isChromeTarget) {
|
|
294
|
+
if (isChromeTarget || isWebServerTarget) {
|
|
285
295
|
// Pin a fixed port so the Chrome origin stays the same between runs.
|
|
286
296
|
// Firebase Auth persists sessions per-origin (IndexedDB) — a random port
|
|
287
297
|
// each run means the user gets logged out every restart.
|
|
@@ -320,7 +330,7 @@ async function runRun(directory, options = {}) {
|
|
|
320
330
|
const envDefine = dartDefines.find((a) => a.startsWith('--dart-define=ENV='));
|
|
321
331
|
const envValue = envDefine ? envDefine.split('=').pop() : null;
|
|
322
332
|
const deviceLabel = options.web
|
|
323
|
-
? 'chrome'
|
|
333
|
+
? (isWebServerTarget ? 'web-server' : 'chrome')
|
|
324
334
|
: options.ios
|
|
325
335
|
? 'ios'
|
|
326
336
|
: options.android
|
|
@@ -346,8 +356,22 @@ async function runRun(directory, options = {}) {
|
|
|
346
356
|
}
|
|
347
357
|
console.log(kleur.dim(` ✦ ${t('run.updateHint.prefix')} `) + kleur.cyan('kasy update') + kleur.dim(` ${t('run.updateHint.suffix')}\n`));
|
|
348
358
|
|
|
359
|
+
// Print the open-in-browser URL only AFTER Flutter signals "ready" — otherwise
|
|
360
|
+
// the user clicks too early and lands on a blank/splash page because the
|
|
361
|
+
// web-server is still compiling.
|
|
362
|
+
const onReady = isWebServerTarget
|
|
363
|
+
? () => {
|
|
364
|
+
const url = `http://localhost:${options.webPort || '5555'}`;
|
|
365
|
+
const modifier = process.platform === 'darwin' ? 'Cmd' : 'Ctrl';
|
|
366
|
+
console.log('');
|
|
367
|
+
console.log(` ${paintLime(`✦ ${t('run.web.open')}: ${url}`)}`);
|
|
368
|
+
console.log(kleur.dim(` ${t('run.web.openHint', { modifier })}`));
|
|
369
|
+
console.log('');
|
|
370
|
+
}
|
|
371
|
+
: undefined;
|
|
372
|
+
|
|
349
373
|
try {
|
|
350
|
-
await spawnFlutterWithSpinner(args, projectDir, t, { raw: Boolean(options.raw) });
|
|
374
|
+
await spawnFlutterWithSpinner(args, projectDir, t, { raw: Boolean(options.raw), onReady });
|
|
351
375
|
} catch (err) {
|
|
352
376
|
if (err.code === 'ENOENT') {
|
|
353
377
|
throw new Error(t('run.error.flutterNotFound'));
|
package/lib/utils/flutter-run.js
CHANGED
|
@@ -125,9 +125,10 @@ function relLogPath(logPath) {
|
|
|
125
125
|
function spawnFlutterWithSpinner(args, projectDir, t, options = {}) {
|
|
126
126
|
const raw = Boolean(options.raw) || !process.stdout.isTTY;
|
|
127
127
|
const log = openRunLog(projectDir);
|
|
128
|
+
const onReady = typeof options.onReady === 'function' ? options.onReady : null;
|
|
128
129
|
|
|
129
|
-
if (raw) return spawnRaw(args, projectDir, t, log);
|
|
130
|
-
return spawnWithSpinner(args, projectDir, t, log);
|
|
130
|
+
if (raw) return spawnRaw(args, projectDir, t, log, onReady);
|
|
131
|
+
return spawnWithSpinner(args, projectDir, t, log, onReady);
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
/**
|
|
@@ -137,16 +138,24 @@ function spawnFlutterWithSpinner(args, projectDir, t, options = {}) {
|
|
|
137
138
|
* Stdin is forwarded directly so hot-reload keys (r/R/q) work without any
|
|
138
139
|
* raw-mode toggling — the user is already on a "dumb" terminal here.
|
|
139
140
|
*/
|
|
140
|
-
function spawnRaw(args, projectDir, t, log) {
|
|
141
|
+
function spawnRaw(args, projectDir, t, log, onReady) {
|
|
141
142
|
return new Promise((resolve, reject) => {
|
|
142
143
|
const proc = spawn('flutter', args, {
|
|
143
144
|
cwd: projectDir,
|
|
144
145
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
145
146
|
});
|
|
146
147
|
|
|
148
|
+
let readyFired = false;
|
|
149
|
+
const fireReady = () => {
|
|
150
|
+
if (readyFired || !onReady) return;
|
|
151
|
+
readyFired = true;
|
|
152
|
+
try { onReady(); } catch (_) {}
|
|
153
|
+
};
|
|
154
|
+
|
|
147
155
|
const tee = (chunk, sink) => {
|
|
148
156
|
sink.write(chunk);
|
|
149
157
|
if (log) log.stream.write(stripAnsi(chunk.toString()));
|
|
158
|
+
if (!readyFired && FLUTTER_READY_RE.test(chunk.toString())) fireReady();
|
|
150
159
|
};
|
|
151
160
|
|
|
152
161
|
proc.stdout.on('data', (c) => tee(c, process.stdout));
|
|
@@ -181,7 +190,7 @@ function spawnRaw(args, projectDir, t, log) {
|
|
|
181
190
|
* ready. On early exit, the buffer is replayed so the user sees the real
|
|
182
191
|
* Flutter error.
|
|
183
192
|
*/
|
|
184
|
-
function spawnWithSpinner(args, projectDir, t, log) {
|
|
193
|
+
function spawnWithSpinner(args, projectDir, t, log, onReady) {
|
|
185
194
|
return new Promise((resolve, reject) => {
|
|
186
195
|
const proc = spawn('flutter', args, {
|
|
187
196
|
cwd: projectDir,
|
|
@@ -208,6 +217,9 @@ function spawnWithSpinner(args, projectDir, t, log) {
|
|
|
208
217
|
clearInterval(tick);
|
|
209
218
|
const total = formatElapsed(startTime);
|
|
210
219
|
spinner.stop(`${t('run.spinner.ready')} ${kleur.dim(`[${total}]`)}`);
|
|
220
|
+
if (onReady) {
|
|
221
|
+
try { onReady(); } catch (_) {}
|
|
222
|
+
}
|
|
211
223
|
for (const chunk of buffer) process.stdout.write(chunk);
|
|
212
224
|
buffer.length = 0;
|
|
213
225
|
// Pipe stdin so the user can type r / R / q to control Flutter.
|
|
@@ -716,6 +716,8 @@ module.exports = {
|
|
|
716
716
|
// run command
|
|
717
717
|
'cli.command.run.description': 'Run your app on phone, simulator, or browser',
|
|
718
718
|
'run.launching': 'Launching Flutter app...',
|
|
719
|
+
'run.web.open': 'Open in your browser',
|
|
720
|
+
'run.web.openHint': '{modifier}+click the link above (or copy/paste it). Use --open to auto-launch a dedicated Flutter Chrome window instead.',
|
|
719
721
|
'run.prompt.pickDevice': 'Multiple devices detected. Which one do you want to run on?',
|
|
720
722
|
'run.warn.nothingSelected': 'No device selected.',
|
|
721
723
|
'run.updateHint.prefix': 'Project improvements available —',
|
|
@@ -749,6 +749,8 @@ module.exports = {
|
|
|
749
749
|
'reset.warn.launcherCacheFailed': 'No se pudo limpiar la caché del launcher.',
|
|
750
750
|
'reset.warn.launcherNotDetected': 'Launcher por defecto no detectado — saltando limpieza de caché.',
|
|
751
751
|
'run.launching': 'Iniciando app Flutter...',
|
|
752
|
+
'run.web.open': 'Abre en tu navegador',
|
|
753
|
+
'run.web.openHint': '{modifier}+clic en el enlace de arriba (o copia/pega). Usa --open para abrir automáticamente una ventana dedicada de Chrome de Flutter.',
|
|
752
754
|
'run.prompt.pickDevice': 'Varios dispositivos detectados. ¿En cuál quieres ejecutar?',
|
|
753
755
|
'run.warn.nothingSelected': 'Ningún dispositivo seleccionado.',
|
|
754
756
|
'run.updateHint.prefix': 'Mejoras disponibles para el proyecto —',
|
|
@@ -749,6 +749,8 @@ module.exports = {
|
|
|
749
749
|
'reset.warn.launcherCacheFailed': 'Não foi possível limpar o cache do launcher.',
|
|
750
750
|
'reset.warn.launcherNotDetected': 'Launcher padrão não detectado — pulando limpeza de cache.',
|
|
751
751
|
'run.launching': 'Iniciando app Flutter...',
|
|
752
|
+
'run.web.open': 'Abra no seu navegador',
|
|
753
|
+
'run.web.openHint': '{modifier}+clique no link acima (ou copie e cole). Use --open pra abrir automaticamente num Chrome dedicado do Flutter.',
|
|
752
754
|
'run.prompt.pickDevice': 'Vários dispositivos detectados. Em qual deles rodar?',
|
|
753
755
|
'run.warn.nothingSelected': 'Nenhum dispositivo selecionado.',
|
|
754
756
|
'run.updateHint.prefix': 'Melhorias disponíveis para o projeto —',
|
package/package.json
CHANGED
|
@@ -428,15 +428,22 @@ class _KasyTextFieldState extends State<KasyTextField> {
|
|
|
428
428
|
),
|
|
429
429
|
)
|
|
430
430
|
: null);
|
|
431
|
+
// Resolved text color for the value rendered inside the field. We bake
|
|
432
|
+
// the disabled fade directly into the color (rather than relying on the
|
|
433
|
+
// Material TextField's own disabled handling) so the disabled state is
|
|
434
|
+
// visually identical across native, web and DatePicker contexts. Without
|
|
435
|
+
// this explicit color, Material would render the value at full opacity
|
|
436
|
+
// because [style] takes precedence over the framework's disabled fade.
|
|
437
|
+
final Color fieldTextColor = isDisabled
|
|
438
|
+
? context.colors.onSurface.withValues(alpha: disabledTextOpacity)
|
|
439
|
+
: context.colors.onSurface;
|
|
431
440
|
final TextStyle fieldTextStyle =
|
|
432
441
|
context.textTheme.bodyLarge?.copyWith(
|
|
433
|
-
color:
|
|
434
|
-
alpha: isDisabled ? disabledTextOpacity : 1,
|
|
435
|
-
),
|
|
442
|
+
color: fieldTextColor,
|
|
436
443
|
fontWeight: FontWeight.w400,
|
|
437
444
|
fontSize: 15,
|
|
438
445
|
) ??
|
|
439
|
-
|
|
446
|
+
TextStyle(fontSize: 15, color: fieldTextColor);
|
|
440
447
|
final InputDecoration decoration = InputDecoration(
|
|
441
448
|
isDense: true,
|
|
442
449
|
hintText: widget.hint,
|
|
@@ -87,6 +87,7 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
87
87
|
RenderObject? _selectedRender;
|
|
88
88
|
Rect? _highlightRect;
|
|
89
89
|
Ticker? _highlightTicker;
|
|
90
|
+
final GlobalKey _overlayKey = GlobalKey(debugLabel: 'devInspectorOverlay');
|
|
90
91
|
|
|
91
92
|
@override
|
|
92
93
|
void initState() {
|
|
@@ -239,12 +240,26 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
239
240
|
setState(() {
|
|
240
241
|
_selectedInfo = picked.info;
|
|
241
242
|
_selectedRender = picked.renderObject;
|
|
242
|
-
_highlightRect = picked.info.boundingBox;
|
|
243
|
+
_highlightRect = _toOverlayLocal(picked.info.boundingBox);
|
|
243
244
|
});
|
|
244
245
|
_startHighlightTicker();
|
|
245
246
|
HapticFeedback.selectionClick();
|
|
246
247
|
}
|
|
247
248
|
|
|
249
|
+
/// Converts a rect expressed in the root view's coordinate space into the
|
|
250
|
+
/// overlay's local space, so it draws correctly even when the overlay sits
|
|
251
|
+
/// inside a transformed parent (e.g. WebDevicePreview's device frame).
|
|
252
|
+
Rect? _toOverlayLocal(Rect rootRect) {
|
|
253
|
+
final RenderObject? renderObject =
|
|
254
|
+
_overlayKey.currentContext?.findRenderObject();
|
|
255
|
+
if (renderObject is! RenderBox) return rootRect;
|
|
256
|
+
if (!renderObject.attached) return rootRect;
|
|
257
|
+
final Offset topLeft = renderObject.globalToLocal(rootRect.topLeft);
|
|
258
|
+
final Offset bottomRight =
|
|
259
|
+
renderObject.globalToLocal(rootRect.bottomRight);
|
|
260
|
+
return Rect.fromPoints(topLeft, bottomRight);
|
|
261
|
+
}
|
|
262
|
+
|
|
248
263
|
void _startHighlightTicker() {
|
|
249
264
|
_highlightTicker ??= createTicker(_onHighlightTick);
|
|
250
265
|
if (!_highlightTicker!.isActive) _highlightTicker!.start();
|
|
@@ -262,8 +277,9 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
262
277
|
setState(_clearSelection);
|
|
263
278
|
return;
|
|
264
279
|
}
|
|
265
|
-
|
|
266
|
-
|
|
280
|
+
final Rect? local = _toOverlayLocal(rect);
|
|
281
|
+
if (_highlightRect != local) {
|
|
282
|
+
setState(() => _highlightRect = local);
|
|
267
283
|
}
|
|
268
284
|
}
|
|
269
285
|
|
|
@@ -378,6 +394,7 @@ class _DevInspectorState extends State<DevInspector>
|
|
|
378
394
|
_onInspectorTap(ev.position),
|
|
379
395
|
child: IgnorePointer(
|
|
380
396
|
child: CustomPaint(
|
|
397
|
+
key: _overlayKey,
|
|
381
398
|
painter: _HighlightPainter(rect: _highlightRect),
|
|
382
399
|
size: Size.infinite,
|
|
383
400
|
),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import 'package:bart/bart/bart_model.dart';
|
|
2
|
+
import 'package:flutter/foundation.dart' show kIsWeb;
|
|
2
3
|
import 'package:flutter/material.dart';
|
|
3
4
|
import 'package:flutter/services.dart';
|
|
4
5
|
import 'package:kasy_kit/components/components.dart';
|
|
@@ -8693,6 +8694,13 @@ class _DatePickerMultiMonthPreview extends StatefulWidget {
|
|
|
8693
8694
|
_DatePickerMultiMonthPreviewState();
|
|
8694
8695
|
}
|
|
8695
8696
|
|
|
8697
|
+
// Breakpoints for the multi-month preview. Below the 2-month threshold the
|
|
8698
|
+
// preview shows an explanation card instead of the calendars — picking dates
|
|
8699
|
+
// across 2 or 3 grids only makes sense on canvases wide enough to lay them
|
|
8700
|
+
// out side by side.
|
|
8701
|
+
const double _kMultiMonthMinWidth2 = 620;
|
|
8702
|
+
const double _kMultiMonthMinWidth3 = 920;
|
|
8703
|
+
|
|
8696
8704
|
class _DatePickerMultiMonthPreviewState
|
|
8697
8705
|
extends State<_DatePickerMultiMonthPreview> {
|
|
8698
8706
|
KasyDateRange? _stay;
|
|
@@ -8700,24 +8708,38 @@ class _DatePickerMultiMonthPreviewState
|
|
|
8700
8708
|
|
|
8701
8709
|
@override
|
|
8702
8710
|
Widget build(BuildContext context) {
|
|
8711
|
+
// Multi-month is a desktop/wide-tablet pattern. On native iOS/Android and
|
|
8712
|
+
// narrow web viewports we don't have horizontal room to lay 2–3 month
|
|
8713
|
+
// grids side by side, so the preview shows a notice instead.
|
|
8714
|
+
final double width = MediaQuery.sizeOf(context).width;
|
|
8715
|
+
final bool fitsTwoMonths = kIsWeb && width >= _kMultiMonthMinWidth2;
|
|
8716
|
+
final bool fitsThreeMonths = kIsWeb && width >= _kMultiMonthMinWidth3;
|
|
8717
|
+
|
|
8718
|
+
if (!fitsTwoMonths) {
|
|
8719
|
+
return _MultiMonthUnavailableNotice();
|
|
8720
|
+
}
|
|
8721
|
+
|
|
8703
8722
|
final KasyDatePickerLocale locale = _datePickerLocaleForApp(context);
|
|
8704
8723
|
return Column(
|
|
8705
8724
|
mainAxisSize: MainAxisSize.min,
|
|
8706
8725
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
8707
8726
|
children: [
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
8719
|
-
|
|
8720
|
-
|
|
8727
|
+
if (fitsThreeMonths) ...[
|
|
8728
|
+
// 3-month layout combined with range selection — the classic
|
|
8729
|
+
// Airbnb / Booking desktop pattern for picking a stay.
|
|
8730
|
+
KasyDatePicker(
|
|
8731
|
+
label: '3 months + range',
|
|
8732
|
+
placeholder: 'Check-in -> Check-out',
|
|
8733
|
+
description:
|
|
8734
|
+
'Pass monthsToShow: 3 to opt in. Best on wide canvases.',
|
|
8735
|
+
selectionMode: KasyDateSelectionMode.range,
|
|
8736
|
+
range: _stay,
|
|
8737
|
+
onRangeChanged: (r) => setState(() => _stay = r),
|
|
8738
|
+
monthsToShow: 3,
|
|
8739
|
+
locale: locale,
|
|
8740
|
+
),
|
|
8741
|
+
const SizedBox(height: KasySpacing.lg),
|
|
8742
|
+
],
|
|
8721
8743
|
// 2-month layout in single-date mode — useful when there's enough
|
|
8722
8744
|
// horizontal room but you still want one tap to commit.
|
|
8723
8745
|
KasyDatePicker(
|
|
@@ -8733,6 +8755,52 @@ class _DatePickerMultiMonthPreviewState
|
|
|
8733
8755
|
}
|
|
8734
8756
|
}
|
|
8735
8757
|
|
|
8758
|
+
/// Friendly placeholder shown when the multi-month preview is opened on a
|
|
8759
|
+
/// viewport too narrow for the layout (native mobile, narrow web). Surfacing
|
|
8760
|
+
/// this instead of hiding the variant keeps the design-system docs honest:
|
|
8761
|
+
/// the feature exists, it just isn't appropriate on small canvases.
|
|
8762
|
+
class _MultiMonthUnavailableNotice extends StatelessWidget {
|
|
8763
|
+
@override
|
|
8764
|
+
Widget build(BuildContext context) {
|
|
8765
|
+
final KasyColors c = context.colors;
|
|
8766
|
+
return Container(
|
|
8767
|
+
padding: const EdgeInsets.all(KasySpacing.lg),
|
|
8768
|
+
decoration: BoxDecoration(
|
|
8769
|
+
color: c.avatarFallbackFill,
|
|
8770
|
+
borderRadius: BorderRadius.circular(KasyRadius.md),
|
|
8771
|
+
),
|
|
8772
|
+
child: Column(
|
|
8773
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
8774
|
+
mainAxisSize: MainAxisSize.min,
|
|
8775
|
+
children: [
|
|
8776
|
+
Row(
|
|
8777
|
+
children: [
|
|
8778
|
+
Icon(KasyIcons.info, size: 18, color: c.primary),
|
|
8779
|
+
const SizedBox(width: KasySpacing.sm),
|
|
8780
|
+
Text(
|
|
8781
|
+
'Multi-month works best on wide screens',
|
|
8782
|
+
style: context.textTheme.labelLarge?.copyWith(
|
|
8783
|
+
fontWeight: FontWeight.w700,
|
|
8784
|
+
color: c.onSurface,
|
|
8785
|
+
),
|
|
8786
|
+
),
|
|
8787
|
+
],
|
|
8788
|
+
),
|
|
8789
|
+
const SizedBox(height: KasySpacing.sm),
|
|
8790
|
+
Text(
|
|
8791
|
+
'The 2 and 3-month layouts need room to render side by side, so '
|
|
8792
|
+
'they are previewed only on web at tablet width or larger. On '
|
|
8793
|
+
'mobile breakpoints and native iOS/Android the picker stays at '
|
|
8794
|
+
'a single month — opt in by passing monthsToShow: 2 or 3 from '
|
|
8795
|
+
'your own desktop screens.',
|
|
8796
|
+
style: context.textTheme.bodyMedium?.copyWith(color: c.muted),
|
|
8797
|
+
),
|
|
8798
|
+
],
|
|
8799
|
+
),
|
|
8800
|
+
);
|
|
8801
|
+
}
|
|
8802
|
+
}
|
|
8803
|
+
|
|
8736
8804
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
8737
8805
|
// DatePicker — Variants (filled, no backdrop, no suffix, no focus border)
|
|
8738
8806
|
// ─────────────────────────────────────────────────────────────────────────────
|