token_text_controller 0.2.1
token_text_controller: ^0.2.1 copied to clipboard
A customizable Flutter text editing controller for detecting, styling, querying, and serializing semantic text tokens.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:token_text_controller/token_text_controller.dart';
void main() => runApp(const ExampleApp());
/// Minimal application demonstrating token-aware editing and suggestions.
class ExampleApp extends StatelessWidget {
/// Creates the package example application.
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
),
home: const TokenEditorPage(),
);
}
}
/// Interactive page containing a token-aware field and rendered preview.
class TokenEditorPage extends StatefulWidget {
/// Creates the token editor example page.
const TokenEditorPage({super.key});
@override
State<TokenEditorPage> createState() => _TokenEditorPageState();
}
class _TokenEditorPageState extends State<TokenEditorPage> {
final controller = TokenTextController(
activeTokens: TokenGroups.assistantChat,
);
final focusNode = FocusNode();
static final suggestions = <TokenKind, List<String>>{
TokenKind.mention: ['@Luis', '@Andrea', '@FlutterDev'],
TokenKind.hashtag: ['#Flutter', '#Messaging', '#Community'],
TokenKind.command: ['/help', '/invite', '/shrug'],
};
@override
void dispose() {
controller.dispose();
focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Token text controller')),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Expanded(
child: ValueListenableBuilder<TextEditingValue>(
valueListenable: controller,
builder: (context, value, child) {
if (value.text.isEmpty) return const SizedBox.shrink();
return Align(
alignment: Alignment.topLeft,
child: TokenText(
value.text,
style: Theme.of(context).textTheme.titleMedium,
),
);
},
),
),
ValueListenableBuilder<TokenQuery?>(
valueListenable: controller.activeQueryListenable,
builder: (context, query, child) {
if (query == null) return const SizedBox.shrink();
final matches = (suggestions[query.kind] ?? const <String>[])
.where(
(value) => value
.substring(1)
.toLowerCase()
.startsWith(query.query.toLowerCase()),
)
.toList(growable: false);
if (matches.isEmpty) return const SizedBox.shrink();
return SizedBox(
height: 48,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: matches.length,
separatorBuilder: (_, _) => const SizedBox(width: 8),
itemBuilder: (context, index) {
final value = matches[index];
return ActionChip(
label: Text(value),
onPressed: () => controller.replaceActiveQuery(
value,
kind: query.kind,
data: {'value': value},
),
);
},
),
);
},
),
TextField(
controller: controller,
focusNode: focusNode,
minLines: 1,
maxLines: 5,
decoration: const InputDecoration(
hintText: 'Message',
border: OutlineInputBorder(),
),
),
],
),
),
),
);
}
}