├── nova4
├── postcss.config.js
├── resources
│ ├── css
│ │ └── card.css
│ └── js
│ │ ├── components
│ │ ├── GroupDivider.vue
│ │ ├── OrButton.vue
│ │ ├── HeroIconPlus.vue
│ │ ├── SelectIcon.vue
│ │ ├── SlideDown.vue
│ │ └── Card.vue
│ │ ├── card.js
│ │ └── flavors
│ │ └── nova4.js
├── dist
│ ├── mix-manifest.json
│ └── css
│ │ ├── card.css.map
│ │ └── card.css
├── tailwind.config.js
├── webpack.mix.js
├── nova.mix.js
└── package.json
├── art
├── refine-nova3.png
└── refine-nova4.png
├── nova3
├── dist
│ ├── mix-manifest.json
│ └── css
│ │ └── card.css
├── resources
│ ├── css
│ │ └── card.css
│ └── js
│ │ ├── components
│ │ ├── GroupDivider.vue
│ │ ├── OrButton.vue
│ │ ├── HeroIconPlus.vue
│ │ ├── DatePicker.vue
│ │ ├── SlideDown.vue
│ │ └── Card.vue
│ │ ├── card.js
│ │ └── flavors
│ │ └── nova.js
├── webpack.mix.js
└── package.json
├── .prettierignore
├── routes
└── api.php
├── pint.json
├── prettier.config.js
├── src
├── RefineCard.php
├── StabilizationController.php
├── CardServiceProvider.php
└── RefinesModels.php
├── package.json
├── composer.json
└── README.md
/nova4/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {}
2 |
--------------------------------------------------------------------------------
/nova4/resources/css/card.css:
--------------------------------------------------------------------------------
1 | @tailwind utilities;
2 |
--------------------------------------------------------------------------------
/art/refine-nova3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aarondfrancis/refine-nova/HEAD/art/refine-nova3.png
--------------------------------------------------------------------------------
/art/refine-nova4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aarondfrancis/refine-nova/HEAD/art/refine-nova4.png
--------------------------------------------------------------------------------
/nova3/dist/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/js/card.js": "/js/card.js",
3 | "/css/card.css": "/css/card.css"
4 | }
5 |
--------------------------------------------------------------------------------
/nova4/dist/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/js/card.js": "/js/card.js",
3 | "/css/card.css": "/css/card.css"
4 | }
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | nova3/dist
2 | nova4/dist
3 | dist/
4 | vendor/
5 | .github/
6 | package-lock.json
7 | composer.lock
8 | composer.json
9 | src
10 | README.md
11 |
--------------------------------------------------------------------------------
/nova3/dist/css/card.css:
--------------------------------------------------------------------------------
1 | /* Shims for Tailwind 0.7.4 */
2 | .first\:rounded-t-lg:first-child {
3 | border-top-left-radius: 0.5rem;
4 | border-top-right-radius: 0.5rem;
5 | }
6 |
7 | .max-h-60 {
8 | max-height: 15rem;
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/nova3/resources/css/card.css:
--------------------------------------------------------------------------------
1 | /* Shims for Tailwind 0.7.4 */
2 | .first\:rounded-t-lg:first-child {
3 | border-top-left-radius: 0.5rem;
4 | border-top-right-radius: 0.5rem;
5 | }
6 |
7 | .max-h-60 {
8 | max-height: 15rem;
9 | }
10 |
--------------------------------------------------------------------------------
/routes/api.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ __('Or') }}
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/nova4/resources/js/components/GroupDivider.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ __('Or') }}
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/pint.json:
--------------------------------------------------------------------------------
1 | {
2 | "preset": "laravel",
3 | "rules": {
4 | "not_operator_with_successor_space": false,
5 | "cast_spaces": false,
6 | "heredoc_to_nowdoc": false,
7 | "phpdoc_summary": false,
8 | "concat_space": {
9 | "spacing": "one"
10 | },
11 | "trailing_comma_in_multiline": false
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/nova3/resources/js/components/OrButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
18 |
--------------------------------------------------------------------------------
/nova4/resources/js/components/OrButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
18 |
--------------------------------------------------------------------------------
/nova4/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // We're going to use the exact Tailwind config that Nova
2 | // uses, but just change where we look for content.
3 | let novaTailwindConfig = require('../vendor/laravel/nova/tailwind.config.js')
4 |
5 | novaTailwindConfig.content = ['./resources/**/*{js,vue}']
6 |
7 | novaTailwindConfig.important = '.refine-nova-card'
8 |
9 | module.exports = novaTailwindConfig
10 |
--------------------------------------------------------------------------------
/nova3/resources/js/components/HeroIconPlus.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
--------------------------------------------------------------------------------
/nova4/resources/js/components/HeroIconPlus.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
--------------------------------------------------------------------------------
/nova3/webpack.mix.js:
--------------------------------------------------------------------------------
1 | const mix = require('laravel-mix')
2 | const BundleAnalyzerPlugin =
3 | require('webpack-bundle-analyzer').BundleAnalyzerPlugin
4 |
5 | mix
6 | .setPublicPath('dist')
7 | .postCss('resources/css/card.css', 'dist/css')
8 | .js('resources/js/card.js', 'js')
9 | .vue()
10 | .webpackConfig({
11 | externals: {
12 | vue: '{use: () => {}}',
13 | },
14 | plugins: [
15 | // new BundleAnalyzerPlugin()
16 | ],
17 | })
18 |
--------------------------------------------------------------------------------
/nova4/webpack.mix.js:
--------------------------------------------------------------------------------
1 | let mix = require('laravel-mix')
2 | let tailwindcss = require('tailwindcss')
3 | const BundleAnalyzerPlugin =
4 | require('webpack-bundle-analyzer').BundleAnalyzerPlugin
5 |
6 | require('./nova.mix')
7 |
8 | mix
9 | .setPublicPath('dist')
10 | .js('resources/js/card.js', 'js')
11 | .vue({ version: 3 })
12 | .postCss('resources/css/card.css', 'dist/css', [
13 | tailwindcss('tailwind.config.js'),
14 | ])
15 | .webpackConfig({
16 | plugins: [
17 | // new BundleAnalyzerPlugin()
18 | ],
19 | })
20 | .nova('hammerstone/refine')
21 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 80,
3 | tabWidth: 2,
4 | useTabs: false,
5 | singleQuote: true,
6 | trailingComma: 'es5',
7 | bracketSpacing: true,
8 | jsxBracketSameLine: false,
9 | semi: false,
10 | requirePragma: false,
11 | proseWrap: 'preserve',
12 | arrowParens: 'avoid',
13 |
14 | overrides: [
15 | {
16 | files: 'resources/css/**/*.css',
17 | options: {
18 | tabWidth: 2,
19 | },
20 | },
21 | {
22 | files: 'resources/js/flavors/**/*.js',
23 | options: {
24 | printWidth: 120,
25 | },
26 | },
27 | ],
28 | }
29 |
--------------------------------------------------------------------------------
/nova4/nova.mix.js:
--------------------------------------------------------------------------------
1 | const mix = require('laravel-mix')
2 | const path = require('path')
3 |
4 | class NovaExtension {
5 | name() {
6 | return 'nova-extension'
7 | }
8 |
9 | register(name) {
10 | this.name = name
11 | }
12 |
13 | webpackConfig(webpackConfig) {
14 | webpackConfig.externals = {
15 | vue: 'Vue',
16 | }
17 |
18 | webpackConfig.resolve.alias = {
19 | ...(webpackConfig.resolve.alias || {}),
20 | '@nova': path.join(__dirname, '../vendor/laravel/nova/resources/js'),
21 | }
22 |
23 | webpackConfig.output = {
24 | uniqueName: this.name,
25 | }
26 | }
27 | }
28 |
29 | mix.extend('nova', new NovaExtension())
30 |
--------------------------------------------------------------------------------
/nova3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "demi": "vue-demi-switch 2",
5 | "dev": "npm run demi && mix",
6 | "watch": "npm run demi && mix watch",
7 | "prod": "npm run demi && mix # @TODO Cant use --production here because it mangles something with vue-demi."
8 | },
9 | "devDependencies": {
10 | "cross-env": "^5.0.0",
11 | "laravel-mix": "^6.0.39",
12 | "vue-loader": "^15.9.8",
13 | "vue-template-compiler": "^2.6.14",
14 | "webpack-bundle-analyzer": "^4.5.0"
15 | },
16 | "dependencies": {
17 | "@hammerstone/refine-vue2": "^0.3.1",
18 | "@vue/composition-api": "^1.7.1",
19 | "store2": "^2.13.1"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/nova4/resources/js/components/SelectIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
22 |
--------------------------------------------------------------------------------
/nova4/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "demi": "vue-demi-switch 3",
5 | "dev": "npm run demi && mix",
6 | "watch": "npm run demi && mix watch",
7 | "prod": "npm run demi && mix # @TODO Cant use --production here because it mangles something with vue-demi.",
8 | "nova:install": "composer update --working-dir \"../\" --with laravel/nova:\"^4.0\" && npm --prefix='../vendor/laravel/nova' ci"
9 | },
10 | "devDependencies": {
11 | "@vue/compiler-sfc": "^3.2.22",
12 | "laravel-mix": "^6.0.41",
13 | "postcss": "^8.3.11",
14 | "tailwindcss": "^3.2.4",
15 | "vue-loader": "^16.8.3"
16 | },
17 | "dependencies": {
18 | "@hammerstone/refine-vue3": "^0.3.1",
19 | "lodash": "^4.17.21",
20 | "store2": "^2.14.2",
21 | "webpack-bundle-analyzer": "^4.7.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/nova3/resources/js/components/DatePicker.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
37 |
--------------------------------------------------------------------------------
/src/RefineCard.php:
--------------------------------------------------------------------------------
1 | withFilter($filter);
19 | }
20 |
21 | public function withFilter($filter)
22 | {
23 | if (is_string($filter)) {
24 | $filter = app($filter);
25 | }
26 |
27 | return $this->withMeta([
28 | 'filter' => $filter,
29 | ]);
30 | }
31 |
32 | /**
33 | * Get the component name for the element.
34 | *
35 | * @return string
36 | */
37 | public function component()
38 | {
39 | return 'refine-nova';
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "refine-nova",
3 | "version": "0.0.0",
4 | "author": "Aaron Francis (http://aaronfrancis.com/)",
5 | "scripts": {
6 | "watch:nova3": "cd nova3 && npm run watch",
7 | "watch:nova4": "cd nova4 && npm run watch",
8 | "watch": "concurrently npm:watch:*",
9 | "dev:nova3": "cd nova3 && npm run dev",
10 | "dev:nova4": "cd nova4 && npm run dev",
11 | "dev": "concurrently npm:dev:*",
12 | "prod:nova3": "cd nova3 && npm run prod",
13 | "prod:nova4": "cd nova4 && npm run prod",
14 | "prod": "concurrently npm:prod:*",
15 | "prep:nova3": "cd nova3 && npm ci",
16 | "prep:nova4": "cd nova4 && npm ci && npm run nova:install",
17 | "prep": "concurrently npm:prep:*",
18 | "build": "npm run prep && npm run prod",
19 | "format": "prettier . -w"
20 | },
21 | "devDependencies": {
22 | "concurrently": "^7.6.0",
23 | "prettier": "^2.8.2"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hammerstone/refine-nova",
3 | "description": "A Laravel Nova integration for the Refine query builder.",
4 | "keywords": [
5 | "laravel",
6 | "nova"
7 | ],
8 | "license": "MIT",
9 | "require": {
10 | "php": "^7.2|^8.0",
11 | "hammerstone/refine-laravel": "^0.3.4|^0.4.0",
12 | "laravel/nova": "^2.12|^3.0|^4.0"
13 | },
14 | "autoload": {
15 | "psr-4": {
16 | "Hammerstone\\Refine\\Nova\\": "src/"
17 | }
18 | },
19 | "repositories": [
20 | {
21 | "type": "composer",
22 | "url": "https://nova.laravel.com"
23 | },
24 | {
25 | "type": "composer",
26 | "url": "https://refine.composer.sh"
27 | }
28 | ],
29 | "extra": {
30 | "laravel": {
31 | "providers": [
32 | "Hammerstone\\Refine\\Nova\\CardServiceProvider"
33 | ]
34 | }
35 | },
36 | "config": {
37 | "sort-packages": true
38 | },
39 | "minimum-stability": "dev",
40 | "prefer-stable": true
41 | }
42 |
--------------------------------------------------------------------------------
/src/StabilizationController.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace Hammerstone\Refine\Nova;
7 |
8 | use Hammerstone\Refine\Filter;
9 | use Hammerstone\Refine\Stabilizers\UrlEncodedStabilizer;
10 | use Illuminate\Http\Request;
11 |
12 | class StabilizationController
13 | {
14 | public function stabilize(Request $request)
15 | {
16 | $filter = Filter::fromState([
17 | 'type' => $request->type,
18 | 'blueprint' => $request->blueprint,
19 | ]);
20 |
21 | $stabilizer = new UrlEncodedStabilizer();
22 |
23 | return [
24 | 'id' => $stabilizer->toStableId($filter),
25 | ];
26 | }
27 |
28 | public function destabilize(Request $request)
29 | {
30 | if (!$request->id) {
31 | return [
32 | 'blueprint' => [],
33 | ];
34 | }
35 |
36 | $filter = (new UrlEncodedStabilizer())->fromStableId($request->id);
37 |
38 | return [
39 | 'blueprint' => $filter->getBlueprint(),
40 | ];
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/nova4/dist/css/card.css.map:
--------------------------------------------------------------------------------
1 | {"version":3,"file":"css/card.css","mappings":"AAAA,0DAAmB,CAAnB,6CAAmB,CAAnB,6CAAmB,CAAnB,8BAAmB,CAAnB,oCAAmB,CAAnB,kCAAmB,CAAnB,kCAAmB,CAAnB,2CAAmB,CAAnB,gBAAmB,CAAnB,yCAAmB,CAAnB,kBAAmB,CAAnB,2CAAmB,CAAnB,2CAAmB,CAAnB,yCAAmB,CAAnB,0CAAmB,CAAnB,uCAAmB,CAAnB,2CAAmB,CAAnB,wCAAmB,CAAnB,yCAAmB,CAAnB,yCAAmB,CAAnB,2CAAmB,CAAnB,0CAAmB,CAAnB,yCAAmB,CAAnB,0CAAmB,CAAnB,sCAAmB,CAAnB,oCAAmB,CAAnB,kDAAmB,CAAnB,sCAAmB,CAAnB,qCAAmB,CAAnB,kCAAmB,CAAnB,qCAAmB,CAAnB,4CAAmB,CAAnB,iCAAmB,CAAnB,oCAAmB,CAAnB,oCAAmB,CAAnB,mCAAmB,CAAnB,oCAAmB,CAAnB,8CAAmB,CAAnB,gDAAmB,CAAnB,uDAAmB,CAAnB,qBAAmB,CAAnB,gBAAmB,CAAnB,iDAAmB,CAAnB,2CAAmB,CAAnB,qDAAmB,CAAnB,kDAAmB,CAAnB,gEAAmB,CAAnB,sCAAmB,CAAnB,8CAAmB,CAAnB,kDAAmB,CAAnB,2CAAmB,CAAnB,sBAAmB,CAAnB,kBAAmB,CAAnB,+CAAmB,CAAnB,iDAAmB,CAAnB,+DAAmB,CAAnB,gCAAmB,CAAnB,0CAAmB,CAAnB,gDAAmB,CAAnB,mDAAmB,CAAnB,4EAAmB,CAAnB,kFAAmB,CAAnB,6CAAmB,CAAnB,sDAAmB,CAAnB,kFAAmB,CAAnB,iDAAmB,CAAnB,mCAAmB,CAAnB,oCAAmB,CAAnB,qCAAmB,CAAnB,yCAAmB,CAAnB,kBAAmB,CAAnB,6CAAmB,CAAnB,kBAAmB,CAAnB,4CAAmB,CAAnB,iBAAmB,CAAnB,yCAAmB,CAAnB,wCAAmB,CAAnB,0CAAmB,CAAnB,wCAAmB,CAAnB,0CAAmB,CAAnB,2CAAmB,CAAnB,0CAAmB,CAAnB,4CAAmB,CAAnB,8CAAmB,CAAnB,4CAAmB,CAAnB,mBAAmB,CAAnB,2CAAmB,CAAnB,gBAAmB,CAAnB,4CAAmB,CAAnB,gDAAmB,CAAnB,8CAAmB,CAAnB,iDAAmB,CAAnB,6CAAmB,CAAnB,mEAAmB,CAAnB,mEAAmB,CAAnB,mEAAmB,CAAnB,mEAAmB,CAAnB,8FAAmB,CAAnB,4FAAmB,CAAnB,yJAAmB,CAAnB,sGAAmB,CAAnB,iGAAmB,CAAnB,mFAAmB,CAAnB,0MAAmB,CAAnB,sDAAmB,CAAnB,qJAAmB,CAAnB,6IAAmB,CAAnB,qKAAmB,CAAnB,kDAAmB,CAAnB,gFCAA,6EDAA,iDCAA,8CDAA,mCCAA,6CDAA,8BCAA,wDDAA,2GCAA,mPDAA,iDCAA,+CDAA,0CCAA,+CDAA,0CCAA,2CDAA,8CCAA,6CDAA,mCCAA,6CDAA,mCCAA,0CDAA,oBCAA,2FDAA,6CCAA,uDDAA,oBCAA,uEDAA,yDCAA,0BDAA,yCCAA,4BDAA,cCAA,C","sources":["webpack://hammerstone/refine/./resources/css/card.css","webpack://hammerstone/refine/"],"sourcesContent":["@tailwind utilities;"],"names":[],"sourceRoot":""}
--------------------------------------------------------------------------------
/src/CardServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->booted(function () {
23 | $this->routes();
24 | });
25 |
26 | if (class_exists('\Hammerstone\Refine\Conditions\Clause')) {
27 | if (is_null(Clause::$resolveComponentUsing)) {
28 | Clause::$resolveComponentUsing = Vue2Frontend::class;
29 | }
30 | }
31 |
32 | $path = Str::startsWith(Nova::version(), '4.') ? 'nova4' : 'nova3';
33 |
34 | Nova::serving(function (ServingNova $event) use ($path) {
35 | Nova::script('refine-nova-card', __DIR__ . "/../$path/dist/js/card.js");
36 | Nova::style('refine-nova', __DIR__ . "/../$path/dist/css/card.css");
37 | });
38 | }
39 |
40 | /**
41 | * Register the card's routes.
42 | *
43 | * @return void
44 | */
45 | protected function routes()
46 | {
47 | if ($this->app->routesAreCached()) {
48 | return;
49 | }
50 |
51 | Route::middleware(['nova'])
52 | ->prefix('nova-vendor/refine-nova')
53 | ->group(__DIR__ . '/../routes/api.php');
54 | }
55 |
56 | /**
57 | * Register any application services.
58 | *
59 | * @return void
60 | */
61 | public function register()
62 | {
63 | //
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/nova3/resources/js/card.js:
--------------------------------------------------------------------------------
1 | import VueCompositionApi from '@vue/composition-api'
2 | import Card from './components/Card'
3 | import { RefinePlugin } from '@hammerstone/refine-vue2'
4 |
5 | // Custom components for Nova 3.
6 | import NovaDatePicker from './components/DatePicker'
7 | import OrButton from './components/OrButton'
8 | import GroupDivider from './components/GroupDivider'
9 |
10 | Nova.booting((Vue, router) => {
11 | // Turn on for to get the Devtools to show up.
12 | // Vue.config.devtools = true;
13 | // __VUE_DEVTOOLS_GLOBAL_HOOK__.Vue = Vue
14 |
15 | // Required for Refine Vue2 to work.
16 | Vue.use(VueCompositionApi)
17 |
18 | Vue.use(RefinePlugin)
19 |
20 | // Custom components for our Nova flavor.
21 | Vue.component('refine-custom-or-button', OrButton)
22 | Vue.component('refine-custom-group-divider', GroupDivider)
23 | Vue.component('refine-custom-date-picker', NovaDatePicker)
24 |
25 | // Main card.
26 | Vue.component('refine-nova', Card)
27 |
28 | attachInterceptors(router)
29 | })
30 |
31 | function attachInterceptors(router) {
32 | // Add a request interceptor so that we can add our Refine query params.
33 | Nova.request().interceptors.request.use(function (config) {
34 | // Instead of checking route patterns, just piggyback onto
35 | // any request where the filters are included, because
36 | // we'll want to Refine all of those requests.
37 | if (_.has(config, 'params.filters')) {
38 | for (let param in router.currentRoute.query) {
39 | // Add every query param that ends in _refine, because
40 | // each resource will start with something different,
41 | // but they all end in _refine.
42 | if (_.endsWith(param, '_refine')) {
43 | config.params[param] = router.currentRoute.query[param]
44 | }
45 | }
46 | }
47 |
48 | return config
49 | })
50 |
51 | // Add a response interceptor so we can catch validation errors.
52 | Nova.request().interceptors.response.use(
53 | function (response) {
54 | return response
55 | },
56 | function (error) {
57 | if (error.response && error.response.status === 422) {
58 | // Emit an event with the error data over to our Card
59 | // component and then let the rejection fall through.
60 | Nova.$emit('validation-error', error.response)
61 | }
62 |
63 | return Promise.reject(error)
64 | }
65 | )
66 | }
67 |
--------------------------------------------------------------------------------
/src/RefinesModels.php:
--------------------------------------------------------------------------------
1 |
4 | */
5 |
6 | namespace Hammerstone\Refine\Nova;
7 |
8 | use Hammerstone\Refine\Blueprints\Blueprint;
9 | use Hammerstone\Refine\Filter;
10 | use Hammerstone\Refine\Stabilizers\UrlEncodedStabilizer;
11 | use Laravel\Nova\Http\Requests\NovaRequest;
12 |
13 | trait RefinesModels
14 | {
15 | /**
16 | * @return string|Filter
17 | */
18 | public static function refineFilter(NovaRequest $request = null)
19 | {
20 | $filter = static::$filter;
21 |
22 | if (is_string($filter)) {
23 | $filter = app($filter, [
24 | 'blueprint' => static::getBlueprint($request)
25 | ]);
26 | }
27 |
28 | return $filter;
29 | }
30 |
31 | /**
32 | * @return RefineCard
33 | */
34 | public static function refineCard(NovaRequest $request = null)
35 | {
36 | return RefineCard::forFilter(static::refineFilter($request));
37 | }
38 |
39 | /**
40 | * @return mixed
41 | */
42 | public static function indexQuery(NovaRequest $request, $query)
43 | {
44 | return static::refine($request, $query);
45 | }
46 |
47 | /**
48 | * @return array|Blueprint
49 | */
50 | public static function defaultBlueprint()
51 | {
52 | return [];
53 | }
54 |
55 | /**
56 | * @return mixed
57 | */
58 | public static function refine(NovaRequest $request, $query)
59 | {
60 | $filter = static::refineFilter($request);
61 |
62 | if (is_string($filter)) {
63 | $filter = app($filter, [
64 | 'blueprint' => static::getBlueprint($request),
65 | ]);
66 | }
67 |
68 | // Typically we would start with the `initialQuery` from the filter,
69 | // but we can't do that in a Nova context, so we give Refine
70 | // the query as-is and let it bind in the user's intent.
71 | $filter->useInitialQuery($query)->bind();
72 |
73 | return $query;
74 | }
75 |
76 | /**
77 | * @return array
78 | */
79 | protected static function getBlueprint(NovaRequest $request = null)
80 | {
81 | // The NovaRequest isn't passed to the `cards` method so it might be null.
82 | // We just need the query params so we use the global request helper.
83 | if ($id = request()->input(static::uriKey() . '_refine')) {
84 | return (new UrlEncodedStabilizer)->fromStableId($id)->getBlueprint();
85 | }
86 |
87 | return static::defaultBlueprint();
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/nova3/resources/js/components/SlideDown.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
101 |
--------------------------------------------------------------------------------
/nova4/resources/js/components/SlideDown.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
101 |
--------------------------------------------------------------------------------
/nova4/resources/js/card.js:
--------------------------------------------------------------------------------
1 | import Card from './components/Card'
2 | import endsWith from 'lodash/endsWith'
3 | import { RefinePlugin } from '@hammerstone/refine-vue3'
4 |
5 | import SelectIcon from './components/SelectIcon'
6 | import OrButton from './components/OrButton'
7 | import GroupDivider from './components/GroupDivider'
8 |
9 | Nova.booting((Vue, store) => {
10 | Vue.component('custom-select-icon', SelectIcon)
11 | Vue.component('custom-or-button', OrButton)
12 | Vue.component('custom-group-divider', GroupDivider)
13 | Vue.use(RefinePlugin, {
14 | showLocators: true,
15 | })
16 |
17 | // Turn on for to get the Devtools to show up.
18 | // Vue.config.devtools = true;
19 | // __VUE_DEVTOOLS_GLOBAL_HOOK__.Vue = Vue
20 |
21 | Vue.component('refine-nova', Card)
22 | })
23 |
24 | // We have to wrap the Nova.request method to be able to attach our
25 | // axios interceptors. In Nova 3, axios was a singleton, but in
26 | // Nova 4 axios gets created on every call of Nova.request, so
27 | // we can't attach the interceptors once to the singleton.
28 | const originalNovaRequest = Nova.request
29 | Nova.request = options => {
30 | // Call the original without any options
31 | // to get back the axios instance.
32 | let axios = originalNovaRequest.call(Nova)
33 |
34 | attachInterceptors(axios)
35 |
36 | // Mimic what's in the original function.
37 | if (options) {
38 | return axios(options)
39 | }
40 |
41 | return axios
42 | }
43 |
44 | function attachInterceptors(axios) {
45 | // Add a request interceptor so that we can add our Refine query params.
46 | axios.interceptors.request.use(function (config) {
47 | let shouldAttach =
48 | // Piggyback onto any request where the filters are included,
49 | // because we'll want to Refine all of those requests.
50 | config?.params?.hasOwnProperty('filters') ||
51 | // Also attach to the cards endpoint, so that we can
52 | // get the right blueprint on initial load.
53 | config?.url?.endsWith('/cards')
54 |
55 | if (shouldAttach) {
56 | // Add every query param that ends in _refine, because
57 | // each resource will start with something different,
58 | // but they all end in _refine.
59 | new URLSearchParams(window.location.search).forEach((value, key) => {
60 | if (endsWith(key, '_refine')) {
61 | if (!config.params) {
62 | config.params = {}
63 | }
64 | config.params[key] = value
65 | }
66 | })
67 | }
68 |
69 | return config
70 | })
71 |
72 | // Add a response interceptor so we can catch validation errors.
73 | axios.interceptors.response.use(
74 | response => response,
75 | error => {
76 | if (error.response && error.response.status === 422) {
77 | // Emit an event with the error data over to our Card
78 | // component and then let the rejection fall through.
79 | Nova.$emit('validation-error', error.response)
80 | }
81 |
82 | return Promise.reject(error)
83 | }
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Refine for Laravel Nova
2 |
3 | [Refine](https://hammerstone.dev) is a powerful, visual query builder for Laravel Nova 3 and 4. Refine is a paid
4 | package, which you can purchase at [hammerstone.dev](https://hammerstone.dev).
5 |
6 | > We also have libraries for Laravel (without Nova) and Ruby on Rails at [hammerstone.dev](https://hammerstone.dev).
7 |
8 | Refine lets you define filterable conditions per resource, and then your users can mix and match them in any way they
9 | want to find exactly what they're looking for.
10 |
11 | ```php
12 | // Create a filter called "UserFilter"
13 | class UserFilter extends Filter
14 | {
15 | public function conditions()
16 | {
17 | return [
18 | // Number condition on the ID column
19 | NumericCondition::make('id', 'ID'),
20 |
21 | // Text condition on the name column
22 | TextCondition::make('name', 'Name'),
23 |
24 | // Boolean condition on the is_subscriber column
25 | BooleanCondition::make('is_subscriber', 'Subscriber'),
26 |
27 | // Option condition on the referral column
28 | OptionCondition::make('referral', 'Referral Source')
29 | ->options([
30 | 'twitter' => 'Twitter',
31 | 'linkedin' => 'LinkedIn',
32 | 'fb' => 'Facebook'
33 | ]),
34 |
35 | // Date condition on the created_at column
36 | DateWithTimeCondition::make('created_at', 'Created At'),
37 | ];
38 | }
39 | }
40 | ```
41 |
42 | 
43 | Refine for Laravel Nova 4
44 |
45 | 
46 | Refine for Laravel Nova 3
47 |
48 | ## Installation
49 |
50 | To use Refine with Nova, you must first require the package `composer require hammerstone/refine-nova`. This will
51 | install `hammerstone/refine-laravel` as well. Since `refine-laravel` is a paid package, you will need to make sure your
52 | credentials are available to composer in the `auth.json` file.
53 |
54 | ## Integration
55 |
56 | Create a `UserFilter` class in your `app\Filters` directory.
57 |
58 | ```php
59 | namespace App\Filters;
60 |
61 | use Hammerstone\Refine\Conditions\NumericCondition;
62 | use Hammerstone\Refine\Filter;
63 |
64 | class UserFilter extends Filter
65 | {
66 | public function conditions()
67 | {
68 | return [
69 | NumericCondition::make('id', 'ID'),
70 |
71 | // @TODO: Add more conditions
72 | ];
73 | }
74 | }
75 | ```
76 |
77 | In your `app\Nova\User` file, you'll need to add the `RefinesModels` trait and reference your newly created filter.
78 |
79 | ```php
80 | use App\Filters\UserFilter;
81 | use Hammerstone\Refine\Nova\RefinesModels;
82 |
83 | class User extends Resource
84 | {
85 | use RefinesModels;
86 |
87 | // ...
88 |
89 | public static $filter = UserFilter::class;
90 | }
91 | ```
92 |
93 | Finally, to show the query builder on the frontend, you'll need to add the Refine card:
94 |
95 | ```php
96 | public function cards(Request $request)
97 | {
98 | return [
99 | static::refineCard()
100 | ];
101 | }
102 | ```
103 |
104 | That's it! You should see the Refine query builder on the Users page in Nova. You can read further documentation about
105 | building filters in the [Refine documentation](https://hammerstone.dev/refine/laravel/docs/main).
--------------------------------------------------------------------------------
/nova3/resources/js/flavors/nova.js:
--------------------------------------------------------------------------------
1 | const novaFlavor = {
2 | emptyGroup: {
3 | class:
4 | 'border rounded-lg shadow border-50 p-2 text-80 bg-white flex items-center justify-between text-sm mb-4',
5 |
6 | wrapper:
7 | 'border rounded-lg shadow border-50 p-2 text-80 bg-white flex items-center justify-between text-sm mb-4',
8 |
9 | addCriterionButton: {
10 | class: 'text-sm flex items-center p-2',
11 | wrapper: {},
12 | icon: 'text-80 h-4 w-4',
13 | text: 'pt-px text-80',
14 | },
15 | },
16 |
17 | group: {
18 | class: 'border rounded-lg shadow border-50',
19 | wrapper: '',
20 |
21 | divider: {
22 | component: 'refine-custom-group-divider',
23 |
24 | // Dont show the divider on the last iteration
25 | class: ({ index, total }) => {
26 | return index === total - 1 ? 'hidden' : 'flex'
27 | },
28 | },
29 |
30 | addCriterionButton: {
31 | class: 'text-80 flex items-center',
32 | wrapper: 'text-sm flex items-center p-2',
33 | icon: 'h-4 w-4 -mt-px',
34 | text: 'mt-px',
35 | },
36 | },
37 |
38 | addGroupButton: {
39 | class: 'text-sm flex items-center p-2 text-80',
40 |
41 | // Use a custom component, because the default is inexplicably bad.
42 | component: 'refine-custom-or-button',
43 | },
44 |
45 | condition: 'first:rounded-t-lg bg-white w-full',
46 |
47 | criterion: {
48 | wrapper: {
49 | order: ['errors', 'selector', 'remove'],
50 | class: 'flex border-b border-50 py-3 pl-2 ',
51 | },
52 | removeCriterionButton: {
53 | class: 'ml-auto py-2 px-4 flex items-center text-60',
54 | icon: 'h-5 w-5',
55 | },
56 | errors: {
57 | class:
58 | 'flex-1 basis-full bg-red-50 border-l-2 border-red-600 text-red-300 px-4 py-2 rounded list-disc list-inside',
59 | error: 'text-red-600 font-semibold',
60 | },
61 | },
62 |
63 | select: {
64 | class: 'relative sm:inline-block w-48 mr-4',
65 | wrapper: 'flex items-start gap-4',
66 | customOptions: {
67 | class: '',
68 | wrapper: 'w-auto pt-4 md:flex md:pt-0',
69 | },
70 | listbox: {
71 | class: ({ isClosed }) => {
72 | return isClosed
73 | ? 'hidden'
74 | : 'max-h-60 shadow list-reset border border-50 rounded-lg overflow-auto'
75 | },
76 |
77 | wrapper: 'absolute z-10 w-full mt-1 bg-white rounded-lg shadow-lg',
78 |
79 | item: {
80 | class: ({ isHighlighted }) => {
81 | return `py-2 pr-8 pl-3 relative cursor-pointer select-none ${
82 | isHighlighted ? 'bg-primary text-white' : ''
83 | }`
84 | },
85 |
86 | text: options =>
87 | `block truncate ${
88 | options.selected ? 'font-semibold' : 'font-normal'
89 | }`,
90 |
91 | icon: {
92 | class: 'w-5 h-5',
93 | wrapper: options =>
94 | `absolute pin-t pin-b pin-r flex items-center pr-4 ${
95 | options.isHighlighted ? 'text-white' : 'text-blue-600'
96 | }`,
97 | },
98 | },
99 | },
100 |
101 | button: {
102 | class: 'form-control form-select w-full text-left',
103 | placeholder: 'block text-gray-300 truncate select-none',
104 | selected: 'block truncate',
105 |
106 | icon: {
107 | class: 'hidden',
108 | wrapper: 'hidden',
109 | },
110 | },
111 |
112 | multi: {
113 | button: {
114 | class:
115 | 'form-control form-select w-full text-left flex items-center overflow-x-auto',
116 |
117 | placeholder: 'block text-gray-300 truncate select-none',
118 |
119 | selected: 'inline-flex mr-1 rounded border border-50 p-1 text-sm',
120 |
121 | icon: {
122 | class: 'hidden',
123 | wrapper: 'hidden',
124 | },
125 |
126 | deselect: {
127 | icon: {
128 | class: 'w-4 h-4',
129 | wrapper: 'flex items-center ml-1 text-gray-500 cursor-pointer',
130 | },
131 | },
132 | },
133 | },
134 | },
135 |
136 | inputs: {
137 | date: {
138 | component: 'refine-custom-date-picker',
139 | relative: {
140 | class: `form-control form-input form-input-bordered mr-4`,
141 | wrapper: 'flex mr-4',
142 | },
143 |
144 | double: {
145 | wrapper: 'flex items-center gap-[1ch]',
146 | joiner: 'mx-2',
147 | },
148 | },
149 |
150 | number: {
151 | class: 'form-control form-input form-input-bordered',
152 |
153 | double: {
154 | wrapper: 'flex items-center gap-[1ch]',
155 | joiner: 'ml-2 mr-2',
156 | },
157 | },
158 |
159 | text: 'form-control form-input form-input-bordered',
160 | },
161 | }
162 |
163 | export default novaFlavor
164 |
--------------------------------------------------------------------------------
/nova3/resources/js/components/Card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 |
13 |
14 |
20 |
23 |
24 |
25 |
26 |
27 |
30 |
{{ collapsedText }}
31 |
34 |
35 |
36 |
37 |
38 |
39 |
198 |
--------------------------------------------------------------------------------
/nova4/resources/js/flavors/nova4.js:
--------------------------------------------------------------------------------
1 | const novaFlavor = {
2 | emptyGroup: {
3 | class:
4 | 'rounded-lg shadow p-2 text-gray-400 bg-white flex items-center justify-between text-sm mb-4 dark:bg-gray-800',
5 |
6 | wrapper: {
7 | class:
8 | 'border rounded-lg shadow border-50 p-2 text-gray-400 bg-white flex items-center justify-between text-sm mb-4',
9 | },
10 |
11 | addCriterionButton: {
12 | class: 'text-sm flex items-center p-2',
13 |
14 | wrapper: {},
15 |
16 | icon: {
17 | class: 'text-gray-400 h-4 w-4',
18 | },
19 |
20 | text: {
21 | class: 'pt-px text-gray-400',
22 | },
23 | },
24 | },
25 |
26 | group: {
27 | class: 'rounded-lg shadow',
28 | wrapper: {
29 | class: '',
30 | },
31 |
32 | divider: {
33 | component: 'custom-group-divider',
34 | // Dont show the divider on the last iteration
35 | class: ({ index, total }) => {
36 | return index === total - 1 ? 'hidden' : 'flex'
37 | },
38 | },
39 |
40 | addCriterionButton: {
41 | wrapper: {
42 | class: 'flex items-center p-2 dark:bg-gray-800 rounded-b-lg',
43 | },
44 |
45 | class: 'text-gray-400 flex items-center',
46 |
47 | icon: {
48 | class: 'h-4 w-4 -mt-px',
49 | },
50 |
51 | text: {
52 | class: 'mt-px text-xs font-bold',
53 | },
54 | },
55 | },
56 |
57 | addGroupButton: {
58 | class: 'text-sm flex items-center p-2 text-gray-400',
59 |
60 | // Use a custom component, because the default is inexplicably bad.
61 | component: 'custom-or-button',
62 | },
63 |
64 | condition: {
65 | class: 'first:rounded-t-lg bg-white dark:bg-gray-800 w-full',
66 | },
67 |
68 | criterion: {
69 | wrapper: {
70 | order: ['selector', 'remove', 'errors'],
71 | class:
72 | 'flex flex-wrap border-b border-gray-100 dark:border-gray-700 py-3 pl-2',
73 | },
74 | removeCriterionButton: {
75 | class:
76 | 'ml-auto py-2 px-4 flex items-center text-gray-300 hover:text-gray-500 dark:hover:text-white',
77 | icon: {
78 | class: 'h-5 w-5',
79 | },
80 | },
81 | errors: {
82 | class: 'w-full list-none help-text mt-2 help-text-error',
83 | error: {
84 | class: '',
85 | },
86 | },
87 | },
88 |
89 | select: {
90 | class: 'relative sm:inline-block w-48 mr-4',
91 |
92 | wrapper: {
93 | class: 'flex items-start',
94 | },
95 |
96 | customOptions: {
97 | class: '',
98 | wrapper: {
99 | class: 'w-auto pt-4 md:flex md:pt-0',
100 | },
101 | },
102 |
103 | listbox: {
104 | class: ({ isClosed }) => {
105 | return isClosed
106 | ? 'hidden'
107 | : 'focus:outline-none max-h-60 shadow list-reset rounded overflow-auto'
108 | },
109 |
110 | wrapper: {
111 | class:
112 | 'absolute z-10 w-full mt-1 bg-white dark:bg-gray-800 rounded shadow-lg',
113 | },
114 |
115 | item: {
116 | class: ({ isHighlighted }) => {
117 | return `py-2 pr-8 pl-3 relative cursor-pointer select-none ${
118 | isHighlighted ? 'bg-primary-600 text-white' : ''
119 | }`
120 | },
121 |
122 | text: {
123 | class: options =>
124 | `block truncate ${
125 | options.selected ? 'font-semibold' : 'font-normal'
126 | }`,
127 | },
128 |
129 | icon: {
130 | class: 'w-5 h-5',
131 | wrapper: {
132 | class: options =>
133 | `absolute top-0 bottom-0 right-0 flex items-center pr-4 ${
134 | !options.isHighlighted
135 | ? 'text-blue-600 dark:text-gray-400'
136 | : 'text-white dark:text-white'
137 | }`,
138 | },
139 | },
140 | },
141 | },
142 |
143 | button: {
144 | class:
145 | 'w-full block form-control form-select text-left form-select-bordered flex items-center',
146 |
147 | placeholder: {
148 | class: 'block text-gray-300 truncate select-none',
149 | },
150 |
151 | selected: {
152 | class: 'block truncate w-full',
153 | },
154 |
155 | icon: {
156 | wrapper: {
157 | class: 'right-0 absolute mr-3',
158 | },
159 | component: 'custom-select-icon',
160 | },
161 | },
162 |
163 | multi: {
164 | button: {
165 | class:
166 | 'form-control form-select w-full text-left flex items-center overflow-x-auto',
167 |
168 | placeholder: {
169 | class: 'block text-gray-300 truncate select-none',
170 | },
171 |
172 | selected: {
173 | class: 'inline-flex mr-1 rounded border border-50 p-1 text-sm',
174 | },
175 |
176 | icon: {
177 | class: 'hidden',
178 | wrapper: {
179 | class: 'hidden',
180 | },
181 | },
182 |
183 | deselect: {
184 | icon: {
185 | class: 'w-4 h-4',
186 |
187 | wrapper: {
188 | class: 'flex items-center ml-1 text-gray-500 cursor-pointer',
189 | },
190 | },
191 | },
192 | },
193 | },
194 | },
195 |
196 | inputs: {
197 | date: {
198 | class: 'form-control form-input form-input-bordered',
199 | relative: {
200 | class: `form-control form-input form-input-bordered mr-4`,
201 |
202 | wrapper: {
203 | class: 'flex mr-4',
204 | },
205 | },
206 |
207 | double: {
208 | wrapper: {
209 | class: 'flex items-center gap-[1ch]',
210 | },
211 |
212 | joiner: {},
213 | },
214 | },
215 |
216 | number: {
217 | class: 'form-control form-input form-input-bordered',
218 |
219 | double: {
220 | wrapper: {
221 | class: 'flex items-center gap-[1ch]',
222 | },
223 |
224 | joiner: {
225 | class: 'ml-2 mr-2',
226 | },
227 | },
228 | },
229 |
230 | text: {
231 | class: 'form-control form-input form-input-bordered',
232 | },
233 | },
234 | }
235 |
236 | export default novaFlavor
237 |
--------------------------------------------------------------------------------
/nova4/resources/js/components/Card.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
25 |
31 |
32 |
33 |
34 |
35 |
38 |
{{ collapsedText }}
39 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
228 |
--------------------------------------------------------------------------------
/nova4/dist/css/card.css:
--------------------------------------------------------------------------------
1 | .refine-nova-card .pointer-events-none {
2 | pointer-events: none
3 | }
4 | .refine-nova-card .absolute {
5 | position: absolute
6 | }
7 | .refine-nova-card .relative {
8 | position: relative
9 | }
10 | .refine-nova-card .top-0 {
11 | top: 0px
12 | }
13 | .refine-nova-card .bottom-0 {
14 | bottom: 0px
15 | }
16 | .refine-nova-card .right-0 {
17 | right: 0px
18 | }
19 | .refine-nova-card .z-10 {
20 | z-index: 10
21 | }
22 | .refine-nova-card .my-2 {
23 | margin-top: 0.5rem;
24 | margin-bottom: 0.5rem
25 | }
26 | .refine-nova-card .mx-2 {
27 | margin-left: 0.5rem;
28 | margin-right: 0.5rem
29 | }
30 | .refine-nova-card .mr-6 {
31 | margin-right: 1.5rem
32 | }
33 | .refine-nova-card .mr-1 {
34 | margin-right: 0.25rem
35 | }
36 | .refine-nova-card .-mt-px {
37 | margin-top: -1px
38 | }
39 | .refine-nova-card .mb-4 {
40 | margin-bottom: 1rem
41 | }
42 | .refine-nova-card .mt-px {
43 | margin-top: 1px
44 | }
45 | .refine-nova-card .ml-auto {
46 | margin-left: auto
47 | }
48 | .refine-nova-card .mt-2 {
49 | margin-top: 0.5rem
50 | }
51 | .refine-nova-card .mr-4 {
52 | margin-right: 1rem
53 | }
54 | .refine-nova-card .mt-1 {
55 | margin-top: 0.25rem
56 | }
57 | .refine-nova-card .mr-3 {
58 | margin-right: 0.75rem
59 | }
60 | .refine-nova-card .ml-1 {
61 | margin-left: 0.25rem
62 | }
63 | .refine-nova-card .ml-2 {
64 | margin-left: 0.5rem
65 | }
66 | .refine-nova-card .mr-2 {
67 | margin-right: 0.5rem
68 | }
69 | .refine-nova-card .block {
70 | display: block
71 | }
72 | .refine-nova-card .flex {
73 | display: flex
74 | }
75 | .refine-nova-card .inline-flex {
76 | display: inline-flex
77 | }
78 | .refine-nova-card .hidden {
79 | display: none
80 | }
81 | .refine-nova-card .h-9 {
82 | height: 2.25rem
83 | }
84 | .refine-nova-card .h-4 {
85 | height: 1rem
86 | }
87 | .refine-nova-card .h-5 {
88 | height: 1.25rem
89 | }
90 | .refine-nova-card .max-h-60 {
91 | max-height: 15rem
92 | }
93 | .refine-nova-card .w-4 {
94 | width: 1rem
95 | }
96 | .refine-nova-card .w-full {
97 | width: 100%
98 | }
99 | .refine-nova-card .w-5 {
100 | width: 1.25rem
101 | }
102 | .refine-nova-card .w-48 {
103 | width: 12rem
104 | }
105 | .refine-nova-card .w-auto {
106 | width: auto
107 | }
108 | .refine-nova-card .flex-shrink-0 {
109 | flex-shrink: 0
110 | }
111 | .refine-nova-card .cursor-pointer {
112 | cursor: pointer
113 | }
114 | .refine-nova-card .select-none {
115 | -webkit-user-select: none;
116 | -moz-user-select: none;
117 | user-select: none
118 | }
119 | .refine-nova-card .list-none {
120 | list-style-type: none
121 | }
122 | .refine-nova-card .flex-wrap {
123 | flex-wrap: wrap
124 | }
125 | .refine-nova-card .items-start {
126 | align-items: flex-start
127 | }
128 | .refine-nova-card .items-center {
129 | align-items: center
130 | }
131 | .refine-nova-card .justify-between {
132 | justify-content: space-between
133 | }
134 | .refine-nova-card .gap-\[1ch\] {
135 | gap: 1ch
136 | }
137 | .refine-nova-card .overflow-auto {
138 | overflow: auto
139 | }
140 | .refine-nova-card .overflow-x-auto {
141 | overflow-x: auto
142 | }
143 | .refine-nova-card .truncate {
144 | overflow: hidden;
145 | text-overflow: ellipsis;
146 | white-space: nowrap
147 | }
148 | .refine-nova-card .rounded {
149 | border-radius: 0.25rem
150 | }
151 | .refine-nova-card .rounded-lg {
152 | border-radius: 0.5rem
153 | }
154 | .refine-nova-card .rounded-b-lg {
155 | border-bottom-right-radius: 0.5rem;
156 | border-bottom-left-radius: 0.5rem
157 | }
158 | .refine-nova-card .border {
159 | border-width: 1px
160 | }
161 | .refine-nova-card .border-t {
162 | border-top-width: 1px
163 | }
164 | .refine-nova-card .border-b {
165 | border-bottom-width: 1px
166 | }
167 | .refine-nova-card .border-gray-100 {
168 | border-color: rgba(var(--colors-gray-100))
169 | }
170 | .refine-nova-card .bg-primary-500 {
171 | background-color: rgba(var(--colors-primary-500))
172 | }
173 | .refine-nova-card .bg-white {
174 | --tw-bg-opacity: 1;
175 | background-color: rgb(255 255 255 / var(--tw-bg-opacity))
176 | }
177 | .refine-nova-card .bg-primary-600 {
178 | background-color: rgba(var(--colors-primary-600))
179 | }
180 | .refine-nova-card .fill-current {
181 | fill: currentColor
182 | }
183 | .refine-nova-card .p-4 {
184 | padding: 1rem
185 | }
186 | .refine-nova-card .p-2 {
187 | padding: 0.5rem
188 | }
189 | .refine-nova-card .p-1 {
190 | padding: 0.25rem
191 | }
192 | .refine-nova-card .px-4 {
193 | padding-left: 1rem;
194 | padding-right: 1rem
195 | }
196 | .refine-nova-card .py-3 {
197 | padding-top: 0.75rem;
198 | padding-bottom: 0.75rem
199 | }
200 | .refine-nova-card .py-2 {
201 | padding-top: 0.5rem;
202 | padding-bottom: 0.5rem
203 | }
204 | .refine-nova-card .pl-4 {
205 | padding-left: 1rem
206 | }
207 | .refine-nova-card .pt-px {
208 | padding-top: 1px
209 | }
210 | .refine-nova-card .pl-2 {
211 | padding-left: 0.5rem
212 | }
213 | .refine-nova-card .pt-4 {
214 | padding-top: 1rem
215 | }
216 | .refine-nova-card .pr-8 {
217 | padding-right: 2rem
218 | }
219 | .refine-nova-card .pl-3 {
220 | padding-left: 0.75rem
221 | }
222 | .refine-nova-card .pr-4 {
223 | padding-right: 1rem
224 | }
225 | .refine-nova-card .text-left {
226 | text-align: left
227 | }
228 | .refine-nova-card .text-right {
229 | text-align: right
230 | }
231 | .refine-nova-card .text-sm {
232 | font-size: 0.875rem;
233 | line-height: 1.25rem
234 | }
235 | .refine-nova-card .text-xs {
236 | font-size: 0.75rem;
237 | line-height: 1rem
238 | }
239 | .refine-nova-card .font-bold {
240 | font-weight: 700
241 | }
242 | .refine-nova-card .font-semibold {
243 | font-weight: 600
244 | }
245 | .refine-nova-card .font-normal {
246 | font-weight: 400
247 | }
248 | .refine-nova-card .text-white {
249 | --tw-text-opacity: 1;
250 | color: rgb(255 255 255 / var(--tw-text-opacity))
251 | }
252 | .refine-nova-card .text-gray-400 {
253 | color: rgba(var(--colors-gray-400))
254 | }
255 | .refine-nova-card .text-gray-300 {
256 | color: rgba(var(--colors-gray-300))
257 | }
258 | .refine-nova-card .text-blue-600 {
259 | color: rgba(var(--colors-blue-600))
260 | }
261 | .refine-nova-card .text-gray-500 {
262 | color: rgba(var(--colors-gray-500))
263 | }
264 | .refine-nova-card .shadow {
265 | --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
266 | --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
267 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)
268 | }
269 | .refine-nova-card .shadow-lg {
270 | --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
271 | --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
272 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)
273 | }
274 | .refine-nova-card .ring-primary-200 {
275 | --tw-ring-color: rgba(var(--colors-primary-200))
276 | }
277 | .refine-nova-card .filter {
278 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)
279 | }
280 | .refine-nova-card .transition {
281 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
282 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
283 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
284 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
285 | transition-duration: 150ms
286 | }
287 | .refine-nova-card .first\:rounded-t-lg:first-child {
288 | border-top-left-radius: 0.5rem;
289 | border-top-right-radius: 0.5rem
290 | }
291 | .refine-nova-card .hover\:bg-primary-400:hover {
292 | background-color: rgba(var(--colors-primary-400))
293 | }
294 | .refine-nova-card .hover\:text-gray-500:hover {
295 | color: rgba(var(--colors-gray-500))
296 | }
297 | .refine-nova-card .focus\:outline-none:focus {
298 | outline: 2px solid transparent;
299 | outline-offset: 2px
300 | }
301 | .refine-nova-card .focus\:ring:focus {
302 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
303 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
304 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000)
305 | }
306 | .refine-nova-card .active\:bg-primary-600:active {
307 | background-color: rgba(var(--colors-primary-600))
308 | }
309 | .refine-nova-card .dark .dark\:border-gray-800 {
310 | border-color: rgba(var(--colors-gray-800))
311 | }
312 | .refine-nova-card .dark .dark\:border-gray-700 {
313 | border-color: rgba(var(--colors-gray-700))
314 | }
315 | .refine-nova-card .dark .dark\:bg-gray-800 {
316 | background-color: rgba(var(--colors-gray-800))
317 | }
318 | .refine-nova-card .dark .dark\:text-gray-800 {
319 | color: rgba(var(--colors-gray-800))
320 | }
321 | .refine-nova-card .dark .dark\:text-gray-400 {
322 | color: rgba(var(--colors-gray-400))
323 | }
324 | .refine-nova-card .dark .dark\:text-white {
325 | --tw-text-opacity: 1;
326 | color: rgb(255 255 255 / var(--tw-text-opacity))
327 | }
328 | .refine-nova-card .dark .dark\:ring-gray-600 {
329 | --tw-ring-color: rgba(var(--colors-gray-600))
330 | }
331 | .refine-nova-card .dark .dark\:hover\:text-white:hover {
332 | --tw-text-opacity: 1;
333 | color: rgb(255 255 255 / var(--tw-text-opacity))
334 | }
335 | @media (min-width: 640px) {
336 | .refine-nova-card .sm\:inline-block {
337 | display: inline-block
338 | }
339 | }
340 | @media (min-width: 768px) {
341 | .refine-nova-card .md\:flex {
342 | display: flex
343 | }
344 | .refine-nova-card .md\:pt-0 {
345 | padding-top: 0px
346 | }
347 | }
348 |
349 |
--------------------------------------------------------------------------------